(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick
2024-10-21 10:07:40 -04:00
58 changed files with 887 additions and 126 deletions

View File

@@ -6,7 +6,17 @@ describe("NumberFormat", function() {
locale: 'en-US'
};
// useGrouping became more nuanced in recent node.
// Its old 'true' value may now be 'always' or 'auto'.
const useGroupingAlways = buildNumberFormat(
{numMode: 'decimal'},
defaultDocSettings
).resolvedOptions().useGrouping as boolean|string;
const useGroupingAuto = (useGroupingAlways === 'always') ? 'auto' : true;
it("should convert Grist options into Intr.NumberFormat", function() {
assert.include([true, 'always'], String(useGroupingAlways));
assert.ownInclude(buildNumberFormat({}, defaultDocSettings).resolvedOptions(), {
minimumFractionDigits: 0,
maximumFractionDigits: 10,
@@ -17,21 +27,21 @@ describe("NumberFormat", function() {
minimumFractionDigits: 0,
maximumFractionDigits: 3,
style: 'decimal',
useGrouping: true,
useGrouping: useGroupingAlways,
});
assert.ownInclude(buildNumberFormat({numMode: 'percent'}, defaultDocSettings).resolvedOptions(), {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
// style: 'percent', // In node v14.17.0 style is 'decimal' (unclear why)
// so we check final formatting instead in this case.
useGrouping: true,
useGrouping: useGroupingAuto,
});
assert.equal(buildNumberFormat({numMode: 'percent'}, defaultDocSettings).format(0.5), '50%');
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, defaultDocSettings).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'USD',
});
assert.ownInclude(buildNumberFormat({numMode: 'scientific'}, defaultDocSettings).resolvedOptions(), {
@@ -73,42 +83,42 @@ describe("NumberFormat", function() {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'EUR',
});
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'en-NZ'}).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'NZD',
});
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'de-CH'}).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'CHF',
});
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'es-AR'}).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'ARS',
});
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'zh-TW'}).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'TWD',
});
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'en-AU'}).resolvedOptions(), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
useGrouping: true,
useGrouping: useGroupingAuto,
currency: 'AUD',
});
});

View File

@@ -149,6 +149,82 @@ describe("AccessRules3", function() {
await assertSaved();
});
it('can have a SeedRule special that refers to columns', async function() {
// Open Access Rules page.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Check seed rule checkbox is unselected.
const seedRule = await driver.find('div.test-rule-special-SeedRule');
const checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), false);
// Now check the box, and see we get the default rule we expect.
await checkbox.click();
await assertChanged();
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
assert.deepEqual(await getRules(seedRule),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D' }]);
assert.equal(await hasExtraAdd(seedRule), true);
// Tweak the seed rule to refer to a column.
await seedRule.find('.test-rule-part .test-rule-add').click();
await enterRulePart(seedRule, 1, 'rec.Year == 1', 'Deny All', 'memo1');
assert.equal(await checkbox.getAttribute('disabled'), 'true');
// New table rules should include the seed rule.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
let fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'rec.Year == 1', perm: '-R-U-C-D', res: 'All', memo: 'memo1'},
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' },
{ formula: 'Everyone Else', perm: '', res: 'All' }]);
assert.equal(await hasExtraAdd(fin), false);
await removeTable(/FinancialsTable/);
// Tweak the seed rule to refer to a column that won't exist.
await enterRulePart(seedRule, 1, 'rec.Unreal == 1', 'Deny All', 'memo1');
assert.equal(await checkbox.getAttribute('disabled'), 'true');
// New table rules should include the seed rule, and show an error.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'rec.Unreal == 1', perm: '-R-U-C-D', res: 'All', memo: 'memo1',
error: 'Invalid columns: Unreal' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' },
{ formula: 'Everyone Else', perm: '', res: 'All' }]);
assert.equal(await hasExtraAdd(fin), false);
await removeTable(/FinancialsTable/);
// Check that returning to the single OWNER rule gets us back to an uncomplicated
// selected checkbox.
await assertChanged();
assert.equal(await checkbox.getAttribute('disabled'), 'true');
assert.equal(await checkbox.isSelected(), false);
await seedRule.find('.test-rule-part .test-rule-remove').click();
assert.equal(await checkbox.getAttribute('disabled'), null);
assert.equal(await checkbox.isSelected(), true);
// Check that removing that rule deselected the checkbox and collapses rule list.
await seedRule.find('.test-rule-part .test-rule-remove').click();
assert.equal(await checkbox.getAttribute('disabled'), null);
assert.equal(await checkbox.isSelected(), false);
await assertSaved();
assert.lengthOf(await seedRule.findAll('.test-rule-set'), 0);
// Expand again, and make sure we are back to default.
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
assert.lengthOf(await seedRule.findAll('.test-rule-set'), 1);
assert.deepEqual(await getRules(seedRule),
[{ formula: 'Everyone', perm: '' }]);
await assertSaved();
});
it('can save and reload SeedRule special', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);

View File

@@ -19,6 +19,10 @@ describe('Importer', function() {
// have tests go faster. Each successful test case should leave the document unchanged.
if (!docUrl || !await gu.testCurrentUrl(docUrl)) {
const session = await gu.session().teamSite.login();
// TODO: tests check colors literally, so need to be in
// light theme - but calling gu.setGristTheme results in
// some problems so right now if you are a dev you just
// need to run these tests in light mode, sorry.
await session.tempDoc(cleanup, 'Hello.grist');
docUrl = await driver.getCurrentUrl();
}
@@ -450,9 +454,9 @@ describe('Importer', function() {
assert.equal(await driver.findWait('.test-importer-preview', 2000).isPresent(), true);
// Check that the merge field select button has a red outline.
assert.equal(
assert.match(
await driver.find('.test-importer-merge-fields-select').getCssValue('border'),
'1px solid rgb(208, 2, 27)'
/solid rgb\(208, 2, 27\)/
);
// Select a merge field, and check that the red outline is gone.
@@ -461,9 +465,9 @@ describe('Importer', function() {
'.test-multi-select-menu .test-multi-select-menu-option',
/Name/
).click();
assert.equal(
assert.match(
await driver.find('.test-importer-merge-fields-select').getCssValue('border'),
'1px solid rgb(217, 217, 217)'
/solid rgb\(217, 217, 217\)/
);
// Hide dropdown
await gu.sendKeys(Key.ESCAPE);
@@ -584,9 +588,9 @@ describe('Importer', function() {
await driver.findContent('.test-importer-source', /UploadedData2Extended.csv/).click();
// Check that it failed, and that the merge fields select button is outlined in red.
assert.equal(
assert.match(
await driver.find('.test-importer-merge-fields-select').getCssValue('border'),
'1px solid rgb(208, 2, 27)'
/solid rgb\(208, 2, 27\)/
);
assert.equal(
await driver.find('.test-importer-source-selected .test-importer-from').getText(),

View File

@@ -139,7 +139,8 @@ namespace gristUtils {
export async function getRules(el: WebElement): Promise<Array<{
formula: string, perm: string,
res?: string,
memo?: string}>> {
memo?: string,
error?: string}>> {
const ruleSets = await el.findAll('.test-rule-set');
const results: Array<{formula: string, perm: string,
res?: string,
@@ -162,9 +163,12 @@ namespace gristUtils {
}
const hasMemo = await part.find('.test-rule-memo').isPresent();
const memo = hasMemo ? await part.find('.test-rule-memo input').value() : undefined;
const hasError = await part.find('.test-rule-error').isPresent();
const error = hasError ? await part.find('.test-rule-error').getText() : undefined;
results.push({formula, perm: permParts.join(''),
...(memo ? {memo} : {}),
...(res ? {res} : {})
...(res ? {res} : {}),
...(error ? {error} : {}),
});
}
}

View File

@@ -1464,6 +1464,7 @@ export function revertChanges(test: () => Promise<void>, invariant: () => any =
export async function redo(optCount: number = 1, optTimeout?: number) {
for (let i = 0; i < optCount; ++i) {
await driver.find('.test-redo').doClick();
await waitForServer(optTimeout);
}
await waitForServer(optTimeout);
}
@@ -2781,11 +2782,11 @@ export function addSamplesForSuite(includeTutorial = false) {
}
export async function openAccountMenu() {
await driver.findWait('.test-dm-account', 1000).click();
await driver.findWait('.test-dm-account', 2000).click();
// Since the AccountWidget loads orgs and the user data asynchronously, the menu
// can expand itself causing the click to land on a wrong button.
await waitForServer();
await driver.findWait('.test-site-switcher-org', 1000);
await driver.findWait('.test-site-switcher-org', 2000);
await driver.sleep(250); // There's still some jitter (scroll-bar? other user accounts?)
}

View File

@@ -35,7 +35,14 @@ export const getPreviewDiffCellValues = stackWrapFunc(async (cols: number[], row
// Helper that waits for the diff preview to finish loading.
export const waitForDiffPreviewToLoad = stackWrapFunc(async (): Promise<void> => {
await gu.waitForServer();
await driver.wait(() => driver.find('.test-importer-preview').isPresent(), 5000);
await gu.waitToPass(async () => {
const preview = (await getPreviewDiffCellValues([0], [1]))[0];
if (preview[0] === undefined && preview[1] === undefined) {
throw new Error('sometimes data is a little slow to show up?');
}
}, 2000);
});
// Helper that gets the list of visible column matching rows to the left of the preview.

View File

@@ -0,0 +1,96 @@
import { AppSettings } from 'app/server/lib/AppSettings';
import { EnvironmentSnapshot } from '../testUtils';
import { assert } from 'chai';
describe('AppSettings', () => {
let appSettings: AppSettings;
let env: EnvironmentSnapshot;
beforeEach(() => {
appSettings = new AppSettings('test');
env = new EnvironmentSnapshot();
});
afterEach(() => {
env.restore();
});
describe('for integers', () => {
function testIntMethod(method: 'readInt' | 'requireInt') {
it('should throw an error if the value is less than the minimum', () => {
process.env.TEST = '4';
assert.throws(() => {
appSettings[method]({ envVar: 'TEST', minValue: 5 });
}, 'value 4 is less than minimum 5');
});
it('should throw an error if the value is greater than the maximum', () => {
process.env.TEST = '6';
assert.throws(() => {
appSettings[method]({ envVar: 'TEST', maxValue: 5 });
}, 'value 6 is greater than maximum 5');
});
it('should throw if the value is NaN', () => {
process.env.TEST = 'not a number';
assert.throws(() => appSettings[method]({ envVar: 'TEST' }), 'not a number does not look like a number');
});
it('should throw if the default value is not finite', () => {
assert.throws(
() => appSettings[method]({ envVar: 'TEST', defaultValue: Infinity }),
'Infinity does not look like a number'
);
});
it('should throw if the default value is not within the range', () => {
assert.throws(
() => appSettings[method]({
envVar: 'TEST',
defaultValue: 6,
minValue: 7,
maxValue: 9,
}),
'value 6 is less than minimum 7'
);
});
it('should return the default value if it is within the range', () => {
const result = appSettings[method]({
envVar: 'TEST',
defaultValue: 5,
minValue: 5,
maxValue: 12
});
assert.strictEqual(result, 5);
});
it('should return the value if it is within the range', () => {
process.env.TEST = '5';
assert.strictEqual(appSettings[method]({ envVar: 'TEST', minValue: 5 }), 5);
});
it('should return the integer value of a float', () => {
process.env.TEST = '5.9';
assert.strictEqual(appSettings[method]({ envVar: 'TEST' }), 5);
});
}
describe('readInt()', () => {
testIntMethod('readInt');
it('should return undefined when no value nor default value is passed', () => {
const result = appSettings.readInt({ envVar: 'TEST', maxValue: 5 });
assert.isUndefined(result);
});
});
describe('requireInt()', () => {
testIntMethod('requireInt');
it('should throw if env variable is not set and no default value is passed', () => {
assert.throws(() => appSettings.requireInt({ envVar: 'TEST' }), 'missing environment variable: TEST');
});
});
});
});

View File

@@ -76,6 +76,8 @@ function makeConfig(username: string): AxiosRequestConfig {
}
describe('DocApi', function () {
const webhooksTestPort = Number(process.env.WEBHOOK_TEST_PORT || 34365);
this.timeout(30000);
testUtils.setTmpLogLevel('error');
let oldEnv: testUtils.EnvironmentSnapshot;
@@ -121,7 +123,7 @@ describe('DocApi', function () {
homeUrl = serverUrl = home.serverUrl;
hasHomeApi = true;
});
testDocApi();
testDocApi({webhooksTestPort});
});
describe('With GRIST_ANON_PLAYGROUND disabled', async () => {
@@ -157,7 +159,7 @@ describe('DocApi', function () {
homeUrl = serverUrl = home.serverUrl;
hasHomeApi = true;
});
testDocApi();
testDocApi({webhooksTestPort});
});
describe('behind a reverse-proxy', function () {
@@ -206,7 +208,7 @@ describe('DocApi', function () {
after(() => tearDown(proxy, [home, docs]));
testDocApi();
testDocApi({webhooksTestPort});
});
async function testCompareDocs(proxy: TestServerReverseProxy, home: TestServer) {
@@ -261,7 +263,7 @@ describe('DocApi', function () {
serverUrl = docs.serverUrl;
hasHomeApi = false;
});
testDocApi();
testDocApi({webhooksTestPort});
});
}
@@ -323,7 +325,10 @@ describe('DocApi', function () {
});
// Contains the tests. This is where you want to add more test.
function testDocApi() {
function testDocApi(settings: {
webhooksTestPort: number,
}) {
const { webhooksTestPort } = settings;
let chimpy: AxiosRequestConfig, kiwi: AxiosRequestConfig,
charon: AxiosRequestConfig, nobody: AxiosRequestConfig, support: AxiosRequestConfig;
@@ -3478,13 +3483,20 @@ function testDocApi() {
});
describe('webhooks related endpoints', async function () {
const serving: Serving = await serveSomething(app => {
app.use(express.json());
app.post('/200', ({body}, res) => {
res.sendStatus(200);
res.end();
});
}, webhooksTestPort);
let serving: Serving;
before(async function () {
serving = await serveSomething(app => {
app.use(express.json());
app.post('/200', ({body}, res) => {
res.sendStatus(200);
res.end();
});
}, webhooksTestPort);
});
after(async function () {
await serving.shutdown();
});
/*
Regression test for old _subscribe endpoint. /docs/{did}/webhooks should be used instead to subscribe
@@ -3577,7 +3589,8 @@ function testDocApi() {
}
}]
},
403, /Column not found notExisting/);
// this check was previously just wrong, was the test not running somehow??
404, /Column not found "notExisting"/);
});
@@ -5385,8 +5398,6 @@ async function getWorkspaceId(api: UserAPIImpl, name: string) {
return workspaces.find((w) => w.name === name)!.id;
}
const webhooksTestPort = Number(process.env.WEBHOOK_TEST_PORT || 34365);
async function setupDataDir(dir: string) {
// we'll be serving Hello.grist content for various document ids, so let's make copies of it in
// tmpDir

View File

@@ -84,15 +84,17 @@ describe("ProxyAgent", function () {
it("should report error when proxy fails", async function() {
// if the proxy isn't listening, fetches produces error messages.
await testProxyServer.dispose();
// Error message depends a little on node version.
const logMessages2 = await captureLog('warn', async () => {
await assert.isRejected(testFetch('/200'), /ECONNREFUSED/);
await assert.isRejected(testFetch('/404'), /ECONNREFUSED/);
await assert.isRejected(testFetch('/200'), /(request.*failed)|(ECONNREFUSED)/);
await assert.isRejected(testFetch('/404'), /(request.*failed)|(ECONNREFUSED)/);
});
// We rely on "ProxyAgent error" message to detect issues with the proxy server.
// Error message depends a little on node version.
assertMatchArray(logMessages2, [
/warn: ProxyAgent error.*ECONNREFUSED/,
/warn: ProxyAgent error.*ECONNREFUSED/,
/warn: ProxyAgent error.*((request.*failed)|(ECONNREFUSED)|(AggregateError))/,
/warn: ProxyAgent error.*((request.*failed)|(ECONNREFUSED)|(AggregateError))/,
]);
});
});

View File

@@ -51,7 +51,8 @@ describe('UnhandledErrors', function() {
}, 1000, 100);
// We expect the server to be dead now.
await assert.isRejected(fetch(`${server.serverUrl}/status`), /failed.*ECONNREFUSED/);
// Error message depends a little on node version.
await assert.isRejected(fetch(`${server.serverUrl}/status`), /(request.*failed)|(ECONNREFUSED)/);
} finally {
await server.stop();