Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add long running test #795

Merged
merged 4 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apis/enigma-mocker/src/__tests__/prop.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,38 @@ describe('getPropFn', () => {
});

describe('async is enabled', () => {
let clock;

before(() => {
clock = sinon.useFakeTimers();
});

afterEach(() => {
clock.restore();
});

it('returns promise', async () => {
const prop = sinon.stub();
prop.returns(500);
const fn = getPropFn(prop, { async: true });
const valuePromise = fn();
expect(valuePromise).to.be.a('promise');
clock.tick(1);
const value = await valuePromise;
expect(value).to.equal(500);
});

it('supports delay', async () => {
const prop = sinon.stub();
prop.returns(600);
const promise = getPropFn(prop, { async: true, delay: 500 })();

clock.tick(510);

return promise.then((value) => {
expect(value).to.equal(600);
});
}).timeout(2000);
});

describe('async is disabled', () => {
Expand Down
10 changes: 8 additions & 2 deletions apis/enigma-mocker/src/from-generic-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import CreateSessionObjectMock from './mocks/create-session-object-mock';
import GetObjectMock from './mocks/get-object-mock';
import GetAppLayoutMock from './mocks/get-app-layout-mock';

/**
* @interface EnigmaMockerOptions
* @property {number} delay Simulate delay (in ms) for calls in enigma-mocker.
*/

/**
* Mocks Engima app functionality. It accepts one / many generic objects as input argument and returns the mocked Enigma app. Each generic object represents one visulization and specifies how it behaves. For example, what layout to use the data to present.
*
Expand All @@ -11,6 +16,7 @@ import GetAppLayoutMock from './mocks/get-app-layout-mock';
* The value for each property is either fixed (string / boolean / number / object) or a function. Arguments are forwarded to the function to allow for greater flexibility. For example, this can be used to return different hypercube data when scrolling in the chart.
*
* @param {Array<object>} genericObjects Generic objects controling behaviour of visualizations.
* @param {EnigmaMockerOptions} options Options
* @returns {Promise<enigma.Doc>}
* @example
* const genericObject = {
Expand All @@ -29,14 +35,14 @@ import GetAppLayoutMock from './mocks/get-app-layout-mock';
* };
* const app = await EnigmaMocker.fromGenericObjects([genericObject]);
*/
export default (genericObjects) => {
export default (genericObjects, options = {}) => {
if (!Array.isArray(genericObjects) || genericObjects.length === 0) {
throw new Error('No "genericObjects" specified');
}

const session = new SessionMock();
const createSessionObject = new CreateSessionObjectMock();
const getObject = new GetObjectMock(genericObjects);
const getObject = new GetObjectMock(genericObjects, options);
const getAppLayout = new GetAppLayoutMock();

const app = {
Expand Down
51 changes: 39 additions & 12 deletions apis/enigma-mocker/src/mocks/get-object-mock.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getPropValue, getPropFn } from '../prop';
import { findAsync } from '../util';

/**
* Properties on `getObject()` operating synchronously.
Expand All @@ -24,26 +23,56 @@ function isPropAsync(name) {
return !PROPS_SYNC.includes(name);
}

/**
* Get `qId` for visualization.
* @param {object} genericObject Generic object describing behaviour of mock
* @returns The `qId`, undefined if not present
*/
function getQId(genericObject) {
const layout = getPropValue(genericObject.getLayout);
return layout.qInfo && layout.qInfo.qId;
}

/**
* Create a mock of a generic object. Mandatory properties are added, functions returns async values where applicable etc.
* @param {object} genericObject Generic object describing behaviour of mock
* @param {EnigmaMockerOptions} options Options.
* @returns The mocked object
*/
function createMock(genericObject) {
function createMock(genericObject, options) {
const qId = getQId(genericObject);
const { delay } = options;
const { id, session, ...props } = genericObject;
return {
const mock = {
id: getPropValue(id, { defaultValue: `object - ${+Date.now()}` }),
session: getPropValue(session, { defaultValue: true }),
on: () => {},
once: () => {},
...Object.entries(props).reduce(
(fns, [name, value]) => ({
...fns,
[name]: getPropFn(value, { async: isPropAsync(name) }),
[name]: getPropFn(value, { async: isPropAsync(name), delay }),
}),
{}
),
};
return { [qId]: mock };
}

/**
* Create mocked objects from list of generic objects.
* @param {Array<object>} genericObjects Generic objects describing behaviour of mock
* @param {EnigmaMockerOptions} options options
* @returns Object with mocks where key is `qId` and value is the mocked object.
*/
function createMocks(genericObjects, options) {
return genericObjects.reduce(
(mocks, genericObject) => ({
...mocks,
...createMock(genericObject, options),
}),
{}
);
}

/**
Expand All @@ -59,25 +88,23 @@ function validate(genericObject) {
if (!genericObject.getLayout) {
throw new Error('Generic object is missing "getLayout"');
}
const layout = getPropValue(genericObject.getLayout);
if (!(layout.qInfo && layout.qInfo.qId)) {
const qId = getQId(genericObject);
if (!qId) {
throw new Error('Generic object is missing "qId" for path "getLayout().qInfo.qId"');
}
}

/**
* Creates mock of `getObject(id)` based on an array of generic objects.
* @param {Array<object>} genericObjects Generic objects.
* @param {EnigmaMockerOptions} options Options.
* @returns Function to retrieve the mocked generic object with the corresponding id.
*/
function GetObjectMock(genericObjects = []) {
function GetObjectMock(genericObjects = [], options = {}) {
genericObjects.forEach(validate);
const genericObjectMocks = genericObjects.map(createMock);
const mocks = createMocks(genericObjects, options);

return async (id) => {
const mock = findAsync(genericObjectMocks, async (m) => (await m.getLayout()).qInfo.qId === id);
return Promise.resolve(mock);
};
return async (id) => Promise.resolve(mocks[id]);
}

export default GetObjectMock;
9 changes: 7 additions & 2 deletions apis/enigma-mocker/src/prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ export const getPropValue = (prop, { args = [], defaultValue } = {}) => {
* @param {object} options Options.
* @param {any} options.defaultValue Default value in case not value is defined in fixture.
* @param {boolean} options.async When `true` the returns value is wrapped in a promise, otherwise the value is directly returned.
* @param {number} options.number Delay before value is returned.
* @returns A fixture property function
*/
export const getPropFn =
(prop, { defaultValue, async = true } = {}) =>
(prop, { defaultValue, async = true, delay = 0 } = {}) =>
(...args) => {
const value = getPropValue(prop, { defaultValue, args });
return async ? Promise.resolve(value) : value;
return async
? new Promise((resolve) => {
setTimeout(() => resolve(value), delay);
})
: value;
};
4 changes: 2 additions & 2 deletions apis/nucleus/src/components/LongRunningQuery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function Cancel({ cancel, translator, ...props }) {
<Progress />
</Grid>
<Grid item>
<Typography variant="h6" align="center">
<Typography variant="h6" align="center" data-tid="update-active">
{translator.get('Object.Update.Active')}
</Typography>
</Grid>
Expand All @@ -52,7 +52,7 @@ export function Retry({ retry, translator, ...props }) {
<WarningTriangle style={{ fontSize: '38px' }} />
</Grid>
<Grid item>
<Typography variant="h6" align="center">
<Typography variant="h6" align="center" data-tid="update-cancelled">
{translator.get('Object.Update.Cancelled')}
</Typography>
</Grid>
Expand Down
15 changes: 15 additions & 0 deletions test/mashup/visualize/life.int.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ describe('object lifecycle', () => {
await waitForTextStatus('[data-tid="error-title"]', 'Incomplete visualization');
});

it('should render long running query', async () => {
const url = getScenarioUrl('long-running');
await page.goto(url);
await waitForTextStatus('[data-tid="update-active"]', 'Updating data');

// the cancel button should appear after 2000ms
await page.click('.njs-cell button');
await waitForTextStatus('[data-tid="update-cancelled"]', 'Data update was cancelled');
// Retry
await waitForTextStatus('.njs-cell button', 'Retry');
await page.click('.njs-cell button');

await waitForTextStatus('.rendered', 'Success!', { timeout: 7000 });
});

// need to fix calc condition view first
it('should show calculation unfulfilled', async () => {
const url = getScenarioUrl('calc-unfulfilled');
Expand Down
118 changes: 117 additions & 1 deletion test/mashup/visualize/scenarios.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,122 @@ scenarios['valid-type'] = {
},
};

scenarios['long-running'] = {
name: 'Long running query',
options: {
delay: 5000,
},
genericObject: {
session: {
getObjectApi() {
return {
cancelRequest() {
return {};
},
};
},
},
getLayout() {
return {
qInfo: {
qId: 'bb8',
qType: 'doesnt matter',
},
qMeta: {
privileges: ['read', 'update', 'delete', 'exportdata'],
},
qSelectionInfo: {},
visualization: 'my-chart',
qHyperCube: {
qSize: {
qcx: 2,
qcy: 1,
},
qDimensionInfo: [
{
qFallbackTitle: '=a',
qApprMaxGlyphCount: 1,
qCardinal: 0,
qSortIndicator: 'N',
qGroupFallbackTitles: ['=a'],
qGroupPos: 0,
qStateCounts: {
qLocked: 0,
qSelected: 0,
qOption: 0,
qDeselected: 0,
qAlternative: 0,
qExcluded: 0,
qSelectedExcluded: 0,
qLockedExcluded: 0,
},
qTags: [],
qDimensionType: 'D',
qGrouping: 'N',
qNumFormat: {
qType: 'U',
qnDec: 0,
qUseThou: 0,
},
qIsAutoFormat: true,
qGroupFieldDefs: ['=a'],
qMin: 'NaN',
qMax: 'NaN',
qAttrExprInfo: [],
qAttrDimInfo: [],
qIsCalculated: true,
qCardinalities: {
qCardinal: 0,
qHypercubeCardinal: 1,
qAllValuesCardinal: -1,
},
},
],
qMeasureInfo: [
{
qFallbackTitle: '=1',
qApprMaxGlyphCount: 1,
qCardinal: 0,
qSortIndicator: 'N',
qNumFormat: {
qType: 'U',
qnDec: 0,
qUseThou: 0,
},
qMin: 1,
qMax: 1,
qIsAutoFormat: true,
qAttrExprInfo: [],
qAttrDimInfo: [],
qTrendLines: [],
},
],
qEffectiveInterColumnSortOrder: [0, 1],
qGrandTotalRow: [
{
qText: '1',
qNum: 1,
qElemNumber: -1,
qState: 'X',
qIsTotalCell: true,
},
],
qDataPages: [],
qPivotDataPages: [],
qStackedDataPages: [],
qMode: 'S',
qNoOfLeftDims: -1,
qTreeNodesOnDim: [],
qColumnOrder: [],
},
};
},
getProperties() {
return {};
},
},
};

scenarios['calc-unfulfilled'] = {
name: 'Calculations unfulfilled',
genericObject: {
Expand Down Expand Up @@ -469,7 +585,7 @@ function getScenario() {

async function render() {
const scenario = getScenario();
const app = await window.enigmaMocker.fromGenericObjects([scenario.genericObject]);
const app = await window.enigmaMocker.fromGenericObjects([scenario.genericObject], scenario.options);
const element = document.querySelector('.viz');
const viz = await configuration(app).render({ element, id: 'bb8' });

Expand Down