Skip to content

Commit 1659160

Browse files
author
brucou
committed
feat(mock): updated mock action and domain, wrote docs, and implemented and added test for domain, with live query and once query distinction. Also created mock for action response but no test written for that.
1 parent e714939 commit 1659160

File tree

9 files changed

+276
-22
lines changed

9 files changed

+276
-22
lines changed

TODO.md

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ cf. https://css-tricks.com/react-router-4/ and investigate if our router can do
4949
- InjectStateInSinks to remove
5050
- delete utils and recheck everything
5151
- reissue patch for all utils (I modified format)
52+
53+
# Learn
54+
- review git rules
5255

5356
# Documentation/Example
5457
- blog : investigate highlighitn with ``` how to add new syntax, or what is the list```

drivers/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rxcc/drivers",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "drivers for component combinators library",
55
"repository": {
66
"type": "git",

drivers/src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './actionDriver/index'
22
export * from './queryDriver/index'
33
export * from './documentDriver/index'
44
export * from './focusDriver/index'
5+
6+
export const LIVE_QUERY_TOKEN = ':'

examples/volunteerApplication/test/app.specs.js

+2
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ QUnit.test("Displays UI", function exec_test(assert) {
269269
[`document!value@${USER_APPLICATION_FORM_INPUT_ZIPCODE_SELECTOR}`]: subjectFactory,
270270
[`document!value@${USER_APPLICATION_FORM_INPUT_OPP_ANSWER_SELECTOR}`]: subjectFactory,
271271
[`document!value@${USER_APPLICATION_FORM_INPUT_TEAM_ANSWER_SELECTOR}`]: subjectFactory,
272+
// TODO : test that with a replay subject instead of normal subject
273+
// TODO : add test for replay subjects in runTestScenario
272274
[`domainQuery!${mockUserAppParams}@${USER_APPLICATION}`]: subjectFactory,
273275
[`domainQuery!${mockTeamsParams}@${TEAMS}`]: subjectFactory,
274276
[`domainQuery!${mockOpportunityParams}@${OPPORTUNITY}`]: subjectFactory,

src/runTestScenario.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ function computeSources(inputs, mockedSourcesHandlers, sourceFactory) {
248248
else if (isMockSource(inputKey)) {
249249
// Case when the inputs are to mock an object
250250
// Ex : 'DOM!selector@event'
251+
// TODO : check there are only two!!!! or take the first one?? DOC it, test it!!
251252
const [sourceName, sourceSpecs] = inputKey.split('!')
252253

253254
// Check the source name is valid (not empty etc.)
@@ -484,7 +485,6 @@ function runTestScenario(inputs, expected, testFn, _settings) {
484485
// Execute the function to be tested (for example a cycle component)
485486
// with the source subjects
486487
console.groupCollapsed('runTestScenario: executing test function');
487-
// const testSinks = testFn(sourcesStruct.sources);
488488
const testSinks = tryCatch(testFn, function testSinksErrorHandler(e, sources) {
489489
console.error('Tested function exited with an exception :', e);
490490
throw e;

test/index.js

+12-11
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import * as QUnit from 'qunitjs';
22
QUnit.dump.maxDepth = 200;
33
window.$ = window.jQuery = require('jquery');
44

5-
import './m.specs'
6-
import './FSM.specs'
7-
import './utils.specs'
8-
import './mEventFactory.specs'
9-
import './mButton.specs'
10-
import './runTestScenario.specs'
11-
import './Switch.specs'
12-
import './Router.specs'
13-
import './ForEach.specs'
14-
import './ListOf.specs'
15-
import './Pipe.specs'
5+
// import './m.specs'
6+
// import './FSM.specs'
7+
// import './utils.specs'
8+
// import './mEventFactory.specs'
9+
// import './mButton.specs'
10+
// import './runTestScenario.specs'
11+
// import './Switch.specs'
12+
// import './Router.specs'
13+
// import './ForEach.specs'
14+
// import './ListOf.specs'
15+
// import './Pipe.specs'
16+
import './mockDomainQuery.specs'

test/mockDomainQuery.specs.js

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as QUnit from "qunitjs"
2+
import { runTestScenario } from "../src/runTestScenario"
3+
import * as Rx from "rx"
4+
import { makeDomainQuerySource } from "../utils/testing/mocks/mockDomainQuery"
5+
import { addPrefix } from "../utils/utils/src/index"
6+
import { LIVE_QUERY_TOKEN } from "../drivers/src"
7+
8+
let $ = Rx.Observable;
9+
10+
const A_SOURCE = 'a_source';
11+
const ANOTHER_SOURCE = 'another_source';
12+
const A_ENTITY = 'a_entity';
13+
const ANOTHER_ENTITY = 'another_entity';
14+
const PREFIX = 'A';
15+
const ANOTHER_PREFIX = 'B';
16+
const YET_ANOTHER_PREFIX = 'C';
17+
const A_JSON_PARAM = {};
18+
const ANYTHING = 'ANYTHING';
19+
const SOMETHING = 'SOMETHING';
20+
const A_STRING = 'A_STRING';
21+
const A_NUMBER = 2;
22+
23+
function analyzeTestResults(assert, done) {
24+
return function analyzeTestResults(actual, expected, message) {
25+
assert.deepEqual(actual, expected, message);
26+
done()
27+
}
28+
}
29+
30+
function subjectFactory() {
31+
return new Rx.Subject()
32+
}
33+
34+
function replaySubjectFactory() {
35+
return new Rx.ReplaySubject(1)
36+
}
37+
38+
function componentWithDomainQuery(sources, settings) {
39+
const { domainQuery, sync } = sources;
40+
41+
// NOTE: both domainQuery.getCurrent(A_ENTITY) should return the same entity in case of live query
42+
// once query will return two different streams
43+
44+
// Live query : ANOTHER_SOURCE should never come to concat sync! subject never completes
45+
// Once query : values before sync except the first should be skipped. Only first value after
46+
// sync should pass
47+
48+
return {
49+
[A_SOURCE]: domainQuery.getCurrent(A_ENTITY, A_JSON_PARAM).map(addPrefix(`${PREFIX}-`)),
50+
[ANOTHER_SOURCE]: domainQuery.getCurrent(A_ENTITY, A_JSON_PARAM).map(addPrefix(`${ANOTHER_PREFIX}-`))
51+
.concat(sync.take(1))
52+
.concat(domainQuery.getCurrent(A_ENTITY, A_JSON_PARAM).map(addPrefix(`${YET_ANOTHER_PREFIX}-`)))
53+
}
54+
}
55+
56+
QUnit.module("mockDomainQuery", {});
57+
58+
59+
QUnit.test("with live query", function exec_test(assert) {
60+
let done = assert.async(2);
61+
62+
const inputs = [
63+
{ sync: { diagram: '---s-', values: { s: ANYTHING } } },
64+
{
65+
[`domainQuery!${JSON.stringify(A_JSON_PARAM)}@${LIVE_QUERY_TOKEN}${A_ENTITY}`]: {
66+
diagram: 'u-v-w', values: { u: null, v: A_STRING, w: A_NUMBER }
67+
}
68+
},
69+
];
70+
71+
const testResults = {
72+
[A_SOURCE]: {
73+
outputs:
74+
[
75+
"A-null",
76+
`A-${A_STRING}`,
77+
`A-${A_NUMBER}`
78+
],
79+
successMessage: 'the live query emits continously its values',
80+
},
81+
[ANOTHER_SOURCE]: {
82+
outputs: [
83+
"B-null",
84+
`B-${A_STRING}`,
85+
`B-${A_NUMBER}`
86+
],
87+
successMessage: 'the live query emits continously its values',
88+
},
89+
};
90+
91+
runTestScenario(inputs, testResults, componentWithDomainQuery, {
92+
tickDuration: 5,
93+
waitForFinishDelay: 20,
94+
analyzeTestResults: analyzeTestResults(assert, done),
95+
mocks: {
96+
domainQuery: makeDomainQuerySource
97+
},
98+
sourceFactory: {
99+
[`domainQuery!${JSON.stringify(A_JSON_PARAM)}@${LIVE_QUERY_TOKEN}${A_ENTITY}`]: replaySubjectFactory,
100+
},
101+
errorHandler: function (err) {
102+
done(err)
103+
}
104+
});
105+
106+
});
107+
108+
QUnit.test("with once query", function exec_test(assert) {
109+
let done = assert.async(2);
110+
111+
const inputs = [
112+
{ sync: { diagram: '---s-', values: { s: ANYTHING } } },
113+
{
114+
[`domainQuery!${JSON.stringify(A_JSON_PARAM)}@${A_ENTITY}`]: {
115+
diagram: 'u-v-w', values: { u: null, v: A_STRING, w: A_NUMBER }
116+
}
117+
},
118+
];
119+
120+
const testResults = {
121+
[A_SOURCE]: {
122+
outputs:
123+
[
124+
"A-null",
125+
],
126+
successMessage: 'once query will return only once',
127+
},
128+
[ANOTHER_SOURCE]: {
129+
outputs: [
130+
`${ANOTHER_PREFIX}-null`,
131+
`${ANYTHING}`,
132+
`${YET_ANOTHER_PREFIX}-${A_NUMBER}`
133+
],
134+
successMessage: 'the live query emits continously its values',
135+
},
136+
};
137+
138+
runTestScenario(inputs, testResults, componentWithDomainQuery, {
139+
tickDuration: 5,
140+
waitForFinishDelay: 20,
141+
analyzeTestResults: analyzeTestResults(assert, done),
142+
mocks: {
143+
domainQuery: makeDomainQuerySource
144+
},
145+
sourceFactory: {
146+
[`domainQuery!${JSON.stringify(A_JSON_PARAM)}@${A_ENTITY}`]: replaySubjectFactory,
147+
},
148+
errorHandler: function (err) {
149+
done(err)
150+
}
151+
});
152+
153+
});
154+
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* NOTE: It is necessary to mock `getResponse` method of `ActionDriver`
3+
* This allows among other things to simulate, without incurring in any effects, live queries
4+
* obtained by concatenating current value and future value updates. For instance :
5+
* function fetchPageNumber(sources, settings) {
6+
* return sources.domainQuery.getCurrent(_index2.PAGE)
7+
* // NOTE : building a live query by fetching the current page number and adding page number
8+
* // change notifications resulting from actions affecting the page
9+
* .concat(sources.domainAction$.getResponse(_index2.PAGE).map((0, _ramda.path)(['response',
10+
* 'page'])))
11+
* .shareReplay(1)
12+
* }
13+
*/
14+
15+
// TODO test it in next version?
16+
import { F, tryCatch } from "ramda"
17+
18+
/**
19+
*
20+
* @param mockedObj
21+
* @param sourceSpecs Has shape jsonStringifyParams@domainObject. It is very important that the
22+
* left side of `sourceSpecs` be a string returned by applying JSON.stringify to the mocked
23+
* args. If there is even a blank space of difference, the mocking function might fail its lookup
24+
* @param stream
25+
* @returns {*|{}}
26+
*/
27+
export function makeDomainActionSource(mockedObj, sourceSpecs, stream) {
28+
const domainObject = sourceSpecs;
29+
30+
if (!isValidContext(domainObject)) {
31+
throw `Invalid context for domain action source : ${domainObject}`
32+
}
33+
34+
// Initialize object hash table if not done already
35+
mockedObj = mockedObj || {};
36+
mockedObj.hashTable = mockedObj.hashTable || {};
37+
mockedObj.hashTable[domainObject] = mockedObj.hashTable[domainObject] || {};
38+
mockedObj.hashTable[domainObject] = stream;
39+
// build the mock anew to incorporate the new stream
40+
mockedObj.getResponse = function (domainObject) {
41+
return mockedObj.hashTable[domainObject];
42+
}
43+
44+
return mockedObj
45+
}
46+
47+
function isValidContext(domainObject) {
48+
// NOTE : ideally we would check that the context actually exists, but we have no way to do
49+
// this, out of a prior registry of contexts... Passing mispelled entities could then become a
50+
// source of writing tests wrong. This can be alleviated by systematically using enum aliases,
51+
// or predefined constants to hold entity's name.
52+
return !!domainObject
53+
}
+48-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { tryCatch, F } from "ramda"
1+
import { F, tryCatch } from "ramda"
2+
import { assertContract } from "../../../utils/contracts/src"
3+
import { format } from "../../../utils/debug/src"
4+
import { LIVE_QUERY_TOKEN } from "../../../drivers/src/index"
5+
import * as Rx from "rx"
6+
7+
const $ = Rx.Observable;
28

39
/**
410
*
@@ -10,27 +16,60 @@ import { tryCatch, F } from "ramda"
1016
* @returns {*|{}}
1117
*/
1218
export function makeDomainQuerySource(mockedObj, sourceSpecs, stream) {
13-
const [jsonParams, domainObject] = sourceSpecs.split('@');
19+
assertContract(isValidDomainQueryMockConfig, [sourceSpecs], `makeDomainQuerySource : Invalid spec for domain query source -- must have one and only one @ character, and both sides around the @ characters must be non-empty! Check ${format(sourceSpecs)}`);
20+
const [jsonParams, unparsedDomainObject] = sourceSpecs.split('@');
21+
const { domainObject, isLiveQuery } = parseDomainObject(unparsedDomainObject);
1422

15-
if (!isValidDomainQuerySourceInput(jsonParams, domainObject)) {
16-
throw `Invalid spec for domain query source : ${sourceSpecs}`
23+
if (domainObject.length === 0) {
24+
// empty string cannot be a domain entity moniker
25+
throw `makeDomainQuerySource: Invalid spec for domain query source -- domain entity cannot be empty string! Check ${format(sourceSpecs)}`
1726
}
1827

1928
// Initialize object hash table if not done already
2029
mockedObj = mockedObj || {};
2130
mockedObj.hashTable = mockedObj.hashTable || {};
2231
mockedObj.hashTable[domainObject] = mockedObj.hashTable[domainObject] || {};
2332
mockedObj.hashTable[domainObject][jsonParams] = mockedObj.hashTable[domainObject][jsonParams] || {};
24-
// register the stream in the hash table
25-
mockedObj.hashTable[domainObject][jsonParams] = stream;
26-
// build the mock anew to incorporate the new stream
27-
mockedObj.getCurrent = function (domainObject, params){
28-
return mockedObj.hashTable[domainObject][JSON.stringify(params)];
33+
34+
if (isLiveQuery) {
35+
mockedObj.hashTable[domainObject][jsonParams] = stream;
36+
mockedObj.getCurrent = mockedObj.getCurrent || function (domainObject, params) {
37+
return mockedObj.hashTable[domainObject][JSON.stringify(params)];
38+
}
39+
}
40+
else {
41+
mockedObj.hashTable[domainObject][jsonParams] = stream;
42+
mockedObj.getCurrent = mockedObj.getCurrent || function (domainObject, params) {
43+
return mockedObj.hashTable[domainObject][JSON.stringify(params)].take(1);
44+
}
2945
}
3046

47+
3148
return mockedObj
3249
}
3350

51+
function parseDomainObject(unparsedDomainObject) {
52+
return unparsedDomainObject.startsWith(LIVE_QUERY_TOKEN)
53+
? {
54+
domainObject: unparsedDomainObject.split(LIVE_QUERY_TOKEN)[1],
55+
isLiveQuery: true
56+
}
57+
: {
58+
domainObject: unparsedDomainObject,
59+
isLiveQuery: false
60+
}
61+
62+
}
63+
64+
function isValidDomainQueryMockConfig(sourceSpecs) {
65+
const preParsed = sourceSpecs.split('@');
66+
const [jsonParams, domainObject] = preParsed;
67+
68+
return preParsed && preParsed.length === 2 && isValidDomainQuerySourceInput(jsonParams, domainObject)
69+
}
70+
3471
function isValidDomainQuerySourceInput(jsonParams, domainObject) {
3572
return domainObject && tryCatch(JSON.parse, F)(jsonParams)
73+
? true
74+
: `isValidDomainQuerySourceInput : either domainObject is falsy or jsonParams is invalid JSON!`
3675
}

0 commit comments

Comments
 (0)