mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding UI for timing API
Summary: Adding new buttons to control the `timing` API and a way to view the results using virtual table features. Test Plan: Added new Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4252
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Sort } from 'app/common/SortSpec';
|
||||
import { Sort, VirtualId } from 'app/common/SortSpec';
|
||||
import { assert } from 'chai';
|
||||
|
||||
const { flipSort: flipColDirection, parseSortColRefs, reorderSortRefs } = Sort;
|
||||
@@ -76,11 +76,22 @@ describe('sortUtil', function () {
|
||||
assert.deepEqual(Sort.setColDirection('2:emptyLast', Sort.DESC), '-2:emptyLast');
|
||||
});
|
||||
|
||||
it('should create column expressions for virtual ids', function () {
|
||||
assert.deepEqual(Sort.setColDirection(VirtualId('test'), Sort.DESC), `-${VirtualId('test')}`);
|
||||
assert.deepEqual(Sort.setColDirection(VirtualId('test'), Sort.ASC), VirtualId('test'));
|
||||
assert.deepEqual(Sort.setColDirection(`-${VirtualId('test')}`, Sort.ASC), VirtualId('test'));
|
||||
assert.deepEqual(Sort.setColDirection(`-${VirtualId('test')}`, Sort.DESC), `-${VirtualId('test')}`);
|
||||
});
|
||||
|
||||
const empty = { emptyLast: false, orderByChoice: false, naturalSort: false };
|
||||
|
||||
it('should parse details', function () {
|
||||
assert.deepEqual(Sort.specToDetails(2), { colRef: 2, direction: Sort.ASC });
|
||||
assert.deepEqual(Sort.specToDetails(-2), { colRef: 2, direction: Sort.DESC });
|
||||
|
||||
assert.deepEqual(Sort.specToDetails(VirtualId('test')), { colRef: VirtualId('test'), direction: Sort.ASC });
|
||||
assert.deepEqual(Sort.specToDetails(`-${VirtualId('test')}`), { colRef: VirtualId('test'), direction: Sort.DESC });
|
||||
|
||||
assert.deepEqual(Sort.specToDetails('-2:emptyLast'),
|
||||
{ ...empty, colRef: 2, direction: Sort.DESC, emptyLast: true });
|
||||
assert.deepEqual(Sort.specToDetails('-2:emptyLast;orderByChoice'), {
|
||||
@@ -93,6 +104,10 @@ describe('sortUtil', function () {
|
||||
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: 2, direction: Sort.ASC }), 2);
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: 2, direction: Sort.DESC }), -2);
|
||||
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: VirtualId('test'), direction: Sort.ASC }), VirtualId('test'));
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: VirtualId('test'), direction: Sort.DESC }), `-${VirtualId('test')}`);
|
||||
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: 2, direction: Sort.ASC, emptyLast: true }), '2:emptyLast');
|
||||
assert.deepEqual(Sort.detailsToSpec({ colRef: 2, direction: Sort.DESC, emptyLast: true }), '-2:emptyLast');
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('CopyPaste', function() {
|
||||
|
||||
// Change the document locale
|
||||
await gu.openDocumentSettings();
|
||||
await driver.findWait('.test-locale-autocomplete', 500).click();
|
||||
await driver.findWait('.test-settings-locale-autocomplete', 500).click();
|
||||
await driver.sendKeys("Germany", Key.ENTER);
|
||||
await gu.waitForServer();
|
||||
await driver.navigate().back();
|
||||
|
||||
@@ -77,10 +77,10 @@ describe('NumericEditor', function() {
|
||||
|
||||
// Set locale for the document.
|
||||
await gu.openDocumentSettings();
|
||||
await driver.findWait('.test-locale-autocomplete', 500).click();
|
||||
await driver.findWait('.test-settings-locale-autocomplete', 500).click();
|
||||
await driver.sendKeys(options.locale, Key.ENTER);
|
||||
await gu.waitForServer();
|
||||
assert.equal(await driver.find('.test-locale-autocomplete input').value(), options.locale);
|
||||
assert.equal(await driver.find('.test-settings-locale-autocomplete input').value(), options.locale);
|
||||
await gu.openPage('Table1');
|
||||
});
|
||||
|
||||
|
||||
196
test/nbrowser/Timing.ts
Normal file
196
test/nbrowser/Timing.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { DocAPI, UserAPI } from "app/common/UserAPI";
|
||||
import difference from 'lodash/difference';
|
||||
import { assert, driver } from "mocha-webdriver";
|
||||
import * as gu from "test/nbrowser/gristUtils";
|
||||
import { setupTestSuite } from "test/nbrowser/testUtils";
|
||||
|
||||
describe("Timing", function () {
|
||||
this.timeout(20000);
|
||||
const cleanup = setupTestSuite();
|
||||
|
||||
let docApi: DocAPI;
|
||||
let userApi: UserAPI;
|
||||
let docId: string;
|
||||
let session: gu.Session;
|
||||
|
||||
before(async () => {
|
||||
session = await gu.session().teamSite.login();
|
||||
docId = await session.tempNewDoc(cleanup);
|
||||
userApi = session.createHomeApi();
|
||||
docApi = userApi.getDocAPI(docId);
|
||||
});
|
||||
|
||||
async function assertOn() {
|
||||
await gu.waitToPass(async () => {
|
||||
assert.equal(await timingText.text(), "Timing is on...");
|
||||
});
|
||||
|
||||
assert.isTrue(await stopTiming.visible());
|
||||
assert.isFalse(await startTiming.present());
|
||||
}
|
||||
|
||||
async function assertOff() {
|
||||
await gu.waitToPass(async () => {
|
||||
assert.equal(await timingText.text(), "Find slow formulas");
|
||||
});
|
||||
assert.isTrue(await startTiming.visible());
|
||||
assert.isFalse(await stopTiming.present());
|
||||
}
|
||||
|
||||
|
||||
it("should allow to start session", async function () {
|
||||
await gu.openDocumentSettings();
|
||||
// Make sure we see the timing button.
|
||||
await assertOff();
|
||||
|
||||
// Start timing.
|
||||
await startTiming.click();
|
||||
|
||||
// Wait for modal.
|
||||
await modal.wait();
|
||||
|
||||
// We have two options.
|
||||
assert.isTrue(await optionStart.visible());
|
||||
assert.isTrue(await optionReload.visible());
|
||||
|
||||
// Start is selected by default.
|
||||
assert.isTrue(await optionStart.checked());
|
||||
assert.isFalse(await optionReload.checked());
|
||||
|
||||
await modalConfirm.click();
|
||||
|
||||
await assertOn();
|
||||
});
|
||||
|
||||
it('should reflect that in the API', async function() {
|
||||
assert.equal(await docApi.timing().then(x => x.status), 'active');
|
||||
});
|
||||
|
||||
it('should stop session from outside', async function() {
|
||||
await docApi.stopTiming();
|
||||
await assertOff();
|
||||
});
|
||||
|
||||
it('should start session from API', async function() {
|
||||
await docApi.startTiming();
|
||||
|
||||
// Add new record through the API (to trigger formula calculations).
|
||||
await userApi.applyUserActions(docId, [
|
||||
['AddRecord', 'Table1', null, {}]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should show result and stop session', async function() {
|
||||
// The stop button is actually stop and show results, and it will open new window in.
|
||||
const myTab = await gu.myTab();
|
||||
const tabs = await driver.getAllWindowHandles();
|
||||
await stopTiming.click();
|
||||
|
||||
// Now new tab will be opened, and the timings will be stopped.
|
||||
await gu.waitToPass(async () => {
|
||||
assert.equal(await docApi.timing().then(x => x.status), 'disabled');
|
||||
});
|
||||
|
||||
// Find the new tab.
|
||||
const newTab = difference(await driver.getAllWindowHandles(), tabs)[0];
|
||||
assert.isDefined(newTab);
|
||||
await driver.switchTo().window(newTab);
|
||||
|
||||
// Sanity check that we see some results.
|
||||
assert.isTrue(await driver.findContentWait('div', 'Formula timer', 1000).isDisplayed());
|
||||
|
||||
await gu.waitToPass(async () => {
|
||||
assert.equal(await gu.getCell(0, 1).getText(), 'Table1');
|
||||
});
|
||||
|
||||
// Switch back to the original tab.
|
||||
await myTab.open();
|
||||
|
||||
// Make sure controls are back to the initial state.
|
||||
await assertOff();
|
||||
|
||||
// Close the new tab.
|
||||
await driver.switchTo().window(newTab);
|
||||
await driver.close();
|
||||
await myTab.open();
|
||||
});
|
||||
|
||||
it("should allow to time the document load", async function () {
|
||||
await assertOff();
|
||||
|
||||
await startTiming.click();
|
||||
await modal.wait();
|
||||
|
||||
// Check that cancel works.
|
||||
await modalCancel.click();
|
||||
assert.isFalse(await modal.present());
|
||||
await assertOff();
|
||||
|
||||
// Open modal once again but this time select reload.
|
||||
await startTiming.click();
|
||||
await optionReload.click();
|
||||
assert.isTrue(await optionReload.checked());
|
||||
await modalConfirm.click();
|
||||
|
||||
// We will see spinner.
|
||||
await gu.waitToPass(async () => {
|
||||
await driver.findContentWait('div', 'Loading timing data.', 1000);
|
||||
});
|
||||
|
||||
// We land on the timing page in the same tab.
|
||||
await gu.waitToPass(async () => {
|
||||
assert.isTrue(await driver.findContentWait('div', 'Formula timer', 1000).isDisplayed());
|
||||
assert.equal(await gu.getCell(0, 1).getText(), 'Table1');
|
||||
});
|
||||
|
||||
// Refreshing this tab will move us to the settings page.
|
||||
await driver.navigate().refresh();
|
||||
await gu.waitForUrl('/settings');
|
||||
});
|
||||
});
|
||||
|
||||
const element = (testId: string) => ({
|
||||
element() {
|
||||
return driver.find(testId);
|
||||
},
|
||||
async wait() {
|
||||
await driver.findWait(testId, 1000);
|
||||
},
|
||||
async visible() {
|
||||
return await this.element().isDisplayed();
|
||||
},
|
||||
async present() {
|
||||
return await this.element().isPresent();
|
||||
}
|
||||
});
|
||||
|
||||
const label = (testId: string) => ({
|
||||
...element(testId),
|
||||
async text() {
|
||||
return this.element().getText();
|
||||
},
|
||||
});
|
||||
|
||||
const button = (testId: string) => ({
|
||||
...element(testId),
|
||||
async click() {
|
||||
await gu.scrollIntoView(this.element());
|
||||
await this.element().click();
|
||||
},
|
||||
});
|
||||
|
||||
const option = (testId: string) => ({
|
||||
...button(testId),
|
||||
async checked() {
|
||||
return 'true' === await this.element().findClosest("label").find("input[type='checkbox']").getAttribute('checked');
|
||||
}
|
||||
});
|
||||
|
||||
const startTiming = button(".test-settings-timing-start");
|
||||
const stopTiming = button(".test-settings-timing-stop");
|
||||
const timingText = label(".test-settings-timing-desc");
|
||||
const modal = element(".test-settings-timing-modal");
|
||||
const optionStart = option('.test-settings-timing-modal-option-adhoc');
|
||||
const optionReload = option('.test-settings-timing-modal-option-reload');
|
||||
const modalConfirm = button('.test-settings-timing-modal-confirm');
|
||||
const modalCancel = button('.test-settings-timing-modal-cancel');
|
||||
@@ -4261,7 +4261,7 @@ function testDocApi() {
|
||||
await notFoundCalled.waitAndReset();
|
||||
|
||||
// But the working endpoint won't be called more then once.
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
|
||||
// Trigger second event.
|
||||
await doc.addRows("Table1", {
|
||||
@@ -4273,13 +4273,13 @@ function testDocApi() {
|
||||
assert.deepEqual(firstRow, 1);
|
||||
|
||||
// But the working endpoint won't be called till we reset the queue.
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
|
||||
// Now reset the queue.
|
||||
await clearQueue(docId);
|
||||
|
||||
assert.isFalse(successCalled.called());
|
||||
assert.isFalse(notFoundCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
notFoundCalled.assertNotCalled();
|
||||
|
||||
// Prepare for new calls.
|
||||
successCalled.reset();
|
||||
@@ -4297,7 +4297,7 @@ function testDocApi() {
|
||||
// And the situation will be the same, the working endpoint won't be called till we reset the queue, but
|
||||
// the error endpoint will be called with the third row multiple times.
|
||||
await notFoundCalled.waitAndReset();
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
|
||||
// Cleanup everything, we will now test request timeouts.
|
||||
await Promise.all(cleanup.map(fn => fn())).finally(() => cleanup.length = 0);
|
||||
@@ -4319,7 +4319,7 @@ function testDocApi() {
|
||||
// Long will be started immediately.
|
||||
await longStarted.waitAndReset();
|
||||
// But it won't be finished.
|
||||
assert.isFalse(longFinished.called());
|
||||
longFinished.assertNotCalled();
|
||||
// It will be aborted.
|
||||
controller.abort();
|
||||
assert.deepEqual(await longFinished.waitAndReset(), [408, 4]);
|
||||
@@ -4333,7 +4333,7 @@ function testDocApi() {
|
||||
// abort it till the end of this test.
|
||||
assert.deepEqual(await successCalled.waitAndReset(), 5);
|
||||
assert.deepEqual(await longStarted.waitAndReset(), 5);
|
||||
assert.isFalse(longFinished.called());
|
||||
longFinished.assertNotCalled();
|
||||
|
||||
// Remember this controller for cleanup.
|
||||
const controller5 = controller;
|
||||
@@ -4343,8 +4343,8 @@ function testDocApi() {
|
||||
B: [true],
|
||||
});
|
||||
// We are now completely stuck on the 5th row webhook.
|
||||
assert.isFalse(successCalled.called());
|
||||
assert.isFalse(longFinished.called());
|
||||
successCalled.assertNotCalled();
|
||||
longFinished.assertNotCalled();
|
||||
// Clear the queue, it will free webhooks requests, but it won't cancel long handler on the external server
|
||||
// so it is still waiting.
|
||||
assert.isTrue((await axios.delete(
|
||||
@@ -4356,8 +4356,8 @@ function testDocApi() {
|
||||
assert.deepEqual(await longFinished.waitAndReset(), [408, 5]);
|
||||
|
||||
// We won't be called for the 6th row at all, as it was stuck and the queue was purged.
|
||||
assert.isFalse(successCalled.called());
|
||||
assert.isFalse(longStarted.called());
|
||||
successCalled.assertNotCalled();
|
||||
longStarted.assertNotCalled();
|
||||
|
||||
// Trigger next event.
|
||||
await doc.addRows("Table1", {
|
||||
@@ -4368,7 +4368,7 @@ function testDocApi() {
|
||||
assert.deepEqual(await successCalled.waitAndReset(), 7);
|
||||
assert.deepEqual(await longStarted.waitAndReset(), 7);
|
||||
// But we are stuck again.
|
||||
assert.isFalse(longFinished.called());
|
||||
longFinished.assertNotCalled();
|
||||
// And we can abort current request from 7th row (6th row was skipped).
|
||||
controller.abort();
|
||||
assert.deepEqual(await longFinished.waitAndReset(), [408, 7]);
|
||||
@@ -4411,7 +4411,7 @@ function testDocApi() {
|
||||
controller.abort();
|
||||
await longFinished.waitAndReset();
|
||||
// The second one is not called.
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
// Triggering next event, we will get only calls to the probe (first webhook).
|
||||
await doc.addRows("Table1", {
|
||||
A: [2],
|
||||
@@ -4438,14 +4438,12 @@ function testDocApi() {
|
||||
await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [
|
||||
['UpdateRecord', 'Table1', newRowIds[0], newValues],
|
||||
], chimpy);
|
||||
await delay(100);
|
||||
};
|
||||
const assertSuccessNotCalled = async () => {
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
successCalled.reset();
|
||||
};
|
||||
const assertSuccessCalled = async () => {
|
||||
assert.isTrue(successCalled.called());
|
||||
await successCalled.waitAndReset();
|
||||
};
|
||||
|
||||
@@ -4460,8 +4458,6 @@ function testDocApi() {
|
||||
B: [true],
|
||||
C: ['c1']
|
||||
});
|
||||
await delay(100);
|
||||
assert.isTrue(successCalled.called());
|
||||
await successCalled.waitAndReset();
|
||||
await modifyColumn({ C: 'c2' });
|
||||
await assertSuccessNotCalled();
|
||||
|
||||
@@ -232,7 +232,8 @@ describe('DocApi2', function() {
|
||||
// And check that we are still on.
|
||||
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
|
||||
assert.equal(resp.status, 200, JSON.stringify(resp.data));
|
||||
assert.deepEqual(resp.data, {status: 'active', timing: []});
|
||||
assert.equal(resp.data.status, 'active');
|
||||
assert.isNotEmpty(resp.data.timing);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,7 +306,7 @@ describe('Webhooks-Proxy', function () {
|
||||
await notFoundCalled.waitAndReset();
|
||||
|
||||
// But the working endpoint won't be called more then once.
|
||||
assert.isFalse(successCalled.called());
|
||||
successCalled.assertNotCalled();
|
||||
|
||||
//Cleanup all
|
||||
await Promise.all(cleanup.map(fn => fn())).finally(() => cleanup.length = 0);
|
||||
|
||||
@@ -1,44 +1,58 @@
|
||||
import {delay} from "bluebird";
|
||||
import {delay} from 'bluebird';
|
||||
import {assert} from 'chai';
|
||||
|
||||
/**
|
||||
* Helper that creates a promise that can be resolved from outside.
|
||||
*
|
||||
* @example
|
||||
* const methodCalled = signal();
|
||||
* setTimeout(() => methodCalled.emit(), 1000);
|
||||
* methodCalled.assertNotCalled(); // won't throw as the method hasn't been called yet
|
||||
* await methodCalled.wait(); // will wait for the method to be called
|
||||
* await methodCalled.wait(); // can be called multiple times
|
||||
* methodCalled.reset(); // resets the signal (so that it can be awaited again)
|
||||
* setTimeout(() => methodCalled.emit(), 3000);
|
||||
* await methodCalled.wait(); // will fail, as we wait only 2 seconds
|
||||
*/
|
||||
export function signal() {
|
||||
let resolve: null | ((data: any) => void) = null;
|
||||
let promise: null | Promise<any> = null;
|
||||
let called = false;
|
||||
return {
|
||||
emit(data: any) {
|
||||
if (!resolve) {
|
||||
throw new Error("signal.emit() called before signal.reset()");
|
||||
}
|
||||
called = true;
|
||||
resolve(data);
|
||||
},
|
||||
async wait() {
|
||||
if (!promise) {
|
||||
throw new Error("signal.wait() called before signal.reset()");
|
||||
}
|
||||
const proms = Promise.race([promise, delay(2000).then(() => {
|
||||
throw new Error("signal.wait() timed out");
|
||||
})]);
|
||||
return await proms;
|
||||
},
|
||||
async waitAndReset() {
|
||||
try {
|
||||
return await this.wait();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
called() {
|
||||
return called;
|
||||
},
|
||||
reset() {
|
||||
called = false;
|
||||
promise = new Promise((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
}
|
||||
};
|
||||
let resolve: null | ((data: any) => void) = null;
|
||||
let promise: null | Promise<any> = null;
|
||||
let called = false;
|
||||
return {
|
||||
emit(data: any) {
|
||||
if (!resolve) {
|
||||
throw new Error("signal.emit() called before signal.reset()");
|
||||
}
|
||||
called = true;
|
||||
resolve(data);
|
||||
},
|
||||
async wait() {
|
||||
if (!promise) {
|
||||
throw new Error("signal.wait() called before signal.reset()");
|
||||
}
|
||||
const proms = Promise.race([
|
||||
promise,
|
||||
delay(2000).then(() => {
|
||||
throw new Error("signal.wait() timed out");
|
||||
}),
|
||||
]);
|
||||
return await proms;
|
||||
},
|
||||
async waitAndReset() {
|
||||
try {
|
||||
return await this.wait();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
assertNotCalled() {
|
||||
assert.isFalse(called);
|
||||
},
|
||||
reset() {
|
||||
called = false;
|
||||
promise = new Promise((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user