@projectcaluma/ember-testing 9.0.0 → 10.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/addon/mirage-graphql/filters/answer.js +13 -0
  2. package/addon/mirage-graphql/filters/base.js +47 -20
  3. package/addon/mirage-graphql/filters/question.js +1 -3
  4. package/addon/mirage-graphql/filters/work-item.js +27 -0
  5. package/addon/mirage-graphql/handler.js +25 -7
  6. package/addon/mirage-graphql/index.js +24 -3
  7. package/addon/mirage-graphql/mocks/answer.js +8 -33
  8. package/addon/mirage-graphql/mocks/base.js +119 -47
  9. package/addon/mirage-graphql/mocks/form.js +25 -100
  10. package/addon/mirage-graphql/mocks/question.js +17 -119
  11. package/addon/mirage-graphql/mocks/work-item.js +72 -0
  12. package/addon/mirage-graphql/schema.graphql +121 -8
  13. package/addon/scenarios/distribution.js +287 -0
  14. package/addon-mirage-support/factories/answer.js +2 -0
  15. package/addon-mirage-support/factories/case.js +6 -5
  16. package/addon-mirage-support/factories/document.js +4 -1
  17. package/addon-mirage-support/factories/file.js +6 -5
  18. package/addon-mirage-support/factories/form.js +3 -0
  19. package/addon-mirage-support/factories/format-validator.js +3 -0
  20. package/addon-mirage-support/factories/option.js +3 -0
  21. package/addon-mirage-support/factories/question.js +19 -0
  22. package/addon-mirage-support/factories/task.js +6 -5
  23. package/addon-mirage-support/factories/work-item.js +7 -5
  24. package/addon-mirage-support/factories/workflow.js +9 -0
  25. package/addon-mirage-support/models/answer.js +3 -2
  26. package/addon-mirage-support/models/case.js +1 -0
  27. package/addon-mirage-support/models/document.js +1 -0
  28. package/addon-mirage-support/models/form.js +1 -1
  29. package/addon-mirage-support/models/question.js +4 -2
  30. package/addon-mirage-support/models/work-item.js +2 -0
  31. package/index.js +1 -1
  32. package/package.json +19 -15
  33. package/addon/mirage-graphql/mocks/case.js +0 -9
  34. package/addon/mirage-graphql/mocks/task.js +0 -55
  35. package/addon/mirage-graphql/resolvers/index.js +0 -16
  36. package/addon/mirage-graphql/serializers/answer.js +0 -14
  37. package/addon/mirage-graphql/serializers/base.js +0 -13
  38. package/addon/mirage-graphql/serializers/case.js +0 -11
  39. package/addon/mirage-graphql/serializers/document.js +0 -11
  40. package/addon/mirage-graphql/serializers/file.js +0 -12
  41. package/addon/mirage-graphql/serializers/form.js +0 -11
  42. package/addon/mirage-graphql/serializers/question.js +0 -14
  43. package/addon/mirage-graphql/serializers/task.js +0 -16
  44. package/addon/mirage-graphql/serializers/work-item.js +0 -11
@@ -0,0 +1,13 @@
1
+ import BaseFilter from "@projectcaluma/ember-testing/mirage-graphql/filters/base";
2
+
3
+ export default class extends BaseFilter {
4
+ questions(records, value, { invert = false }) {
5
+ return records.filter(
6
+ (record) => invert !== value.includes(record.questionId)
7
+ );
8
+ }
9
+
10
+ question(records, value, { invert = false }) {
11
+ return this.questions(records, [value], { invert });
12
+ }
13
+ }
@@ -1,39 +1,66 @@
1
+ import { camelize } from "@ember/string";
2
+
1
3
  export default class {
2
- constructor(type, collection, db) {
4
+ constructor(type) {
3
5
  this.type = type;
4
- this.collection = collection;
5
- this.db = db;
6
6
  }
7
7
 
8
- _getFilterFns(filters) {
9
- return Object.entries(filters).map(([name, value]) => {
10
- const fn = this[name];
8
+ _getFilterFns(rawFilters) {
9
+ const filters = Array.isArray(rawFilters)
10
+ ? // new format
11
+ rawFilters
12
+ .filter((filter) => Object.keys(filter).length !== 0) // filter out empty filters
13
+ .map((filter) => {
14
+ const entries = Object.entries(filter);
15
+ const key = entries[0][0];
16
+ const value = entries[0][1];
17
+ const options = entries
18
+ .slice(1)
19
+ .reduce((opts, [k, v]) => ({ ...opts, [k]: v }), {});
20
+
21
+ return { key, value, options };
22
+ })
23
+ : // old format
24
+ Object.entries(rawFilters).map(([key, value]) => ({
25
+ key,
26
+ value,
27
+ }));
28
+
29
+ return filters.map(({ key, value, options = {} }) => {
30
+ const fn = this[key];
11
31
 
12
32
  return typeof fn === "function"
13
- ? (records) => fn.call(this, records, value)
33
+ ? (records) => fn.call(this, records, value, options)
14
34
  : (records) => records;
15
35
  });
16
36
  }
17
37
 
38
+ sort(records, order) {
39
+ if (!order) return records;
40
+
41
+ return records.sort((a, b) => {
42
+ return (
43
+ order
44
+ .map((o) => {
45
+ const attr = camelize(o.attribute.toLowerCase());
46
+ const direction = o.direction === "ASC" ? -1 : 1;
47
+
48
+ return (b[attr] - a[attr]) * direction;
49
+ })
50
+ .find((result) => result !== 0) ?? 0
51
+ );
52
+ });
53
+ }
54
+
18
55
  filter(records, filters) {
19
- // flatten array of filters to find filter functions
20
- const filterObj = Array.isArray(filters)
21
- ? filters.length
22
- ? Object.assign(...filters)
23
- : {}
24
- : filters;
25
-
26
- return this._getFilterFns(filterObj).reduce(
56
+ return this._getFilterFns(filters.filter ?? filters).reduce(
27
57
  (recs, fn) => fn(recs),
28
- records
58
+ this.sort(records, filters?.order)
29
59
  );
30
60
  }
31
61
 
32
62
  find(records, filters) {
33
- return (
34
- this._getFilterFns(filters).reduce((recs, fn) => fn(recs), records)[0] ||
35
- null
36
- );
63
+ return this.filter(records, filters)[0] || null;
37
64
  }
38
65
 
39
66
  slug(records, value) {
@@ -12,10 +12,8 @@ export default class extends BaseFilter {
12
12
  }
13
13
 
14
14
  excludeForms(records, value) {
15
- const forms = this.db.forms.filter(({ slug }) => value.includes(slug));
16
-
17
15
  return records.filter(
18
- ({ formIds }) => !forms.some(({ id }) => (formIds || []).includes(id))
16
+ ({ formIds }) => !value.some((id) => (formIds || []).includes(id))
19
17
  );
20
18
  }
21
19
  }
@@ -0,0 +1,27 @@
1
+ import BaseFilter from "@projectcaluma/ember-testing/mirage-graphql/filters/base";
2
+
3
+ export default class extends BaseFilter {
4
+ status(records, value, { invert = false }) {
5
+ return records.filter(({ status }) => invert !== (status === value));
6
+ }
7
+
8
+ tasks(records, value, { invert = false }) {
9
+ return records.filter((record) => invert !== value.includes(record.taskId));
10
+ }
11
+
12
+ task(records, value, { invert = false }) {
13
+ return this.tasks(records, [value], { invert });
14
+ }
15
+
16
+ controllingGroups(records, value, { invert = false }) {
17
+ return records.filter((record) =>
18
+ value.every((g) => invert !== record.controllingGroups?.includes(g))
19
+ );
20
+ }
21
+
22
+ addressedGroups(records, value, { invert = false }) {
23
+ return records.filter((record) =>
24
+ value.every((g) => invert !== record.addressedGroups?.includes(g))
25
+ );
26
+ }
27
+ }
@@ -1,25 +1,35 @@
1
1
  import { classify } from "@ember/string";
2
2
  import { singularize } from "ember-inflector";
3
3
  import { graphql } from "graphql";
4
+ import {
5
+ GraphQLDate as Date,
6
+ GraphQLDateTime as DateTime,
7
+ } from "graphql-iso-date";
4
8
  import { addMockFunctionsToSchema, makeExecutableSchema } from "graphql-tools";
5
- import moment from "moment";
6
9
 
7
10
  import { Mock } from "@projectcaluma/ember-testing/mirage-graphql";
8
- import resolvers from "@projectcaluma/ember-testing/mirage-graphql/resolvers";
9
- import rawSchema from "@projectcaluma/ember-testing/mirage-graphql/schema.graphql";
11
+ import typeDefs from "@projectcaluma/ember-testing/mirage-graphql/schema.graphql";
10
12
 
11
13
  export default function (server) {
12
14
  return function ({ db }, request) {
13
15
  const mocks = db._collections.reduce((m, { name }) => {
14
16
  const cls = classify(singularize(name));
15
- const mock = new Mock(cls, db[name], db, server);
17
+ const mock = new Mock(cls, server);
16
18
 
17
19
  return { ...m, ...mock.getHandlers() };
18
20
  }, {});
19
21
 
20
22
  const schema = makeExecutableSchema({
21
- typeDefs: rawSchema,
22
- resolvers,
23
+ typeDefs,
24
+ resolvers: {
25
+ Date,
26
+ DateTime,
27
+ GenericScalar: {
28
+ serialize(value) {
29
+ return typeof value === "string" ? JSON.parse(value) : value;
30
+ },
31
+ },
32
+ },
23
33
  resolverValidationOptions: { requireResolversForResolveType: false },
24
34
  });
25
35
 
@@ -29,10 +39,18 @@ export default function (server) {
29
39
  schema,
30
40
  mocks: {
31
41
  ...mocks,
32
- Date: () => moment().format(moment.HTML5_FMT.DATE),
33
42
  JSONString: () => JSON.stringify({}),
34
43
  GenericScalar: () => ({}),
35
44
  Node: (_, { id }) => ({ __typename: atob(id).split(":")[0] }),
45
+ SelectedOption: ({ value }) => {
46
+ const option = server.schema.options.findBy({ slug: value });
47
+
48
+ return {
49
+ slug: value,
50
+ label: option.label,
51
+ __typename: "SelectedOption",
52
+ };
53
+ },
36
54
  },
37
55
  preserveResolvers: false,
38
56
  });
@@ -1,4 +1,4 @@
1
- import { dasherize } from "@ember/string";
1
+ import { dasherize, classify } from "@ember/string";
2
2
  import require from "require";
3
3
 
4
4
  const importTypeOrBase = (path, type) => {
@@ -28,8 +28,29 @@ export const register = (tpl) => (target, name, descriptor) => {
28
28
  return descriptor;
29
29
  };
30
30
 
31
- export const Serializer = function (type, ...args) {
32
- return new (importTypeOrBase("./serializers", type))(type, ...args);
31
+ export const serialize = (deserialized = {}, type) => {
32
+ const __typename = [deserialized.type?.toLowerCase(), type]
33
+ .filter(Boolean)
34
+ .map(classify)
35
+ .join("");
36
+
37
+ return {
38
+ ...deserialized,
39
+ id: btoa(`${__typename}:${deserialized.id}`),
40
+ __typename,
41
+ };
42
+ };
43
+
44
+ export const deserialize = (serialized) => {
45
+ let decodedId = serialized.id;
46
+
47
+ try {
48
+ decodedId = atob(serialized.id).split(":")[1] || serialized.id;
49
+ } catch (e) {
50
+ // this is expected most times
51
+ }
52
+
53
+ return { ...serialized, ...(decodedId ? { id: decodedId } : {}) };
33
54
  };
34
55
 
35
56
  export const Filter = function (type, ...args) {
@@ -4,46 +4,21 @@ import { register } from "@projectcaluma/ember-testing/mirage-graphql";
4
4
  import BaseMock from "@projectcaluma/ember-testing/mirage-graphql/mocks/base";
5
5
 
6
6
  export default class extends BaseMock {
7
- @register("Answer")
8
- handleAnswer({ __typename }) {
9
- return { __typename };
10
- }
11
-
12
7
  _handleSaveDocumentAnswer(
13
8
  _,
14
- {
15
- question: questionSlug,
16
- document: documentId,
17
- clientMutationId,
18
- value,
19
- type,
20
- }
9
+ { question: questionId, document: documentId, value, type }
21
10
  ) {
22
- const questionId = this.db.questions.findBy({ slug: questionSlug }).id;
23
-
24
11
  const answer = this.collection.findBy({ questionId, documentId });
25
12
 
26
- const res = this.handleSavePayload.fn.call(this, _, {
13
+ return this.handleSavePayload.fn.call(this, _, {
27
14
  input: {
28
- id: answer && answer.id,
15
+ id: answer?.id,
29
16
  type,
30
17
  value,
31
18
  documentId,
32
19
  questionId,
33
- clientMutationId,
34
20
  },
35
21
  });
36
-
37
- // Default answers don't have a document.
38
- if (res.answer.documentId) {
39
- const doc = this.db.documents.findBy({ id: res.answer.documentId });
40
-
41
- this.db.documents.update(doc.id, {
42
- answerIds: [...new Set([...(doc.answerIds || []), res.answer.id])],
43
- });
44
- }
45
-
46
- return res;
47
22
  }
48
23
 
49
24
  @register("SaveDocumentStringAnswerPayload")
@@ -51,7 +26,7 @@ export default class extends BaseMock {
51
26
  handleSaveDocumentStringAnswer(_, { input }) {
52
27
  return this._handleSaveDocumentAnswer(_, {
53
28
  ...input,
54
- value: String(input.value),
29
+ value: input.value ? String(input.value) : null,
55
30
  type: "STRING",
56
31
  });
57
32
  }
@@ -61,7 +36,7 @@ export default class extends BaseMock {
61
36
  handleSaveIntegerAnswer(_, { input }) {
62
37
  return this._handleSaveDocumentAnswer(_, {
63
38
  ...input,
64
- value: parseInt(input.value),
39
+ value: input.value ? parseInt(input.value) : null,
65
40
  type: "INTEGER",
66
41
  });
67
42
  }
@@ -71,7 +46,7 @@ export default class extends BaseMock {
71
46
  handleSaveFloatAnswer(_, { input }) {
72
47
  return this._handleSaveDocumentAnswer(_, {
73
48
  ...input,
74
- value: parseFloat(input.value),
49
+ value: input.value ? parseFloat(input.value) : null,
75
50
  type: "FLOAT",
76
51
  });
77
52
  }
@@ -81,7 +56,7 @@ export default class extends BaseMock {
81
56
  handleSaveListAnswer(_, { input }) {
82
57
  return this._handleSaveDocumentAnswer(_, {
83
58
  ...input,
84
- value: [...input.value].map(String),
59
+ value: input.value ? [...input.value].map(String) : null,
85
60
  type: "LIST",
86
61
  });
87
62
  }
@@ -90,7 +65,7 @@ export default class extends BaseMock {
90
65
  handleSaveFileAnswer(_, { input }) {
91
66
  return this._handleSaveDocumentAnswer(_, {
92
67
  ...input,
93
- value: { metadata: { object_name: input.value } },
68
+ value: input.value ? { metadata: { object_name: input.value } } : null,
94
69
  type: "FILE",
95
70
  });
96
71
  }
@@ -1,24 +1,70 @@
1
- import { camelize, dasherize } from "@ember/string";
1
+ import { camelize, dasherize, classify } from "@ember/string";
2
+ import { singularize, pluralize } from "ember-inflector";
3
+ import faker from "faker";
2
4
  import { MockList } from "graphql-tools";
3
5
 
4
6
  import {
5
7
  Filter,
6
- Serializer,
7
8
  register,
9
+ serialize,
10
+ deserialize,
8
11
  } from "@projectcaluma/ember-testing/mirage-graphql";
9
12
 
13
+ export const ANSWER_TYPES = [
14
+ "DATE",
15
+ "FILE",
16
+ "FLOAT",
17
+ "INTEGER",
18
+ "LIST",
19
+ "STRING",
20
+ "TABLE",
21
+ ];
22
+
23
+ export const QUESTION_TYPES = [
24
+ "ACTION_BUTTON",
25
+ "CALCULATED_FLOAT",
26
+ "CHOICE",
27
+ "DATE",
28
+ "DYNAMIC_CHOICE",
29
+ "DYNAMIC_MULTIPLE_CHOICE",
30
+ "FILE",
31
+ "FLOAT",
32
+ "FORM",
33
+ "INTEGER",
34
+ "MULTIPLE_CHOICE",
35
+ "STATIC",
36
+ "TABLE",
37
+ "TEXT",
38
+ "TEXTAREA",
39
+ ];
40
+
41
+ export const TASK_TYPES = [
42
+ "SIMPLE",
43
+ "COMPLETE_WORKFLOW_FORM",
44
+ "COMPLETE_TASK_FORM",
45
+ ];
46
+
47
+ export const TYPE_MAPPING = {
48
+ Answer: ANSWER_TYPES,
49
+ Question: QUESTION_TYPES,
50
+ Task: TASK_TYPES,
51
+ };
52
+
10
53
  export default class {
11
- constructor(type, collection, db, server, ...args) {
54
+ constructor(type, server) {
12
55
  this.type = type;
13
- this.collection = collection;
14
- this.db = db;
15
56
  this.server = server;
57
+ this.schema = server.schema;
58
+
59
+ this.filter = new Filter(type);
60
+ }
16
61
 
17
- this.filter = new Filter(type, collection, db, server, ...args);
18
- this.serializer = new Serializer(type, collection, db, server, ...args);
62
+ get collection() {
63
+ return this.schema[pluralize(camelize(this.type))];
19
64
  }
20
65
 
21
66
  getHandlers() {
67
+ const types = TYPE_MAPPING[this.type];
22
68
  const handlers = (target) => {
23
69
  const proto = Reflect.getPrototypeOf(target);
24
70
  const res = Object.values(proto);
@@ -37,10 +83,26 @@ export default class {
37
83
  ...handlers,
38
84
  // Mocks can have multiple handlers per type.
39
85
  ...handler.__handlerFor.reduce((targets, target) => {
86
+ const handlerName = target.replace(/\{type\}/, this.type);
87
+ const baseHandlerName = handlerName.replace(/\{subtype\}/, "");
88
+ const fn = (...args) => handler.fn.apply(this, args);
89
+
90
+ const newHandlers = types
91
+ ? types.reduce(
92
+ (typeHandlers, type) => ({
93
+ ...typeHandlers,
94
+ [handlerName.replace(
95
+ /\{subtype\}/,
96
+ classify(type.toLowerCase())
97
+ )]: fn,
98
+ }),
99
+ {}
100
+ )
101
+ : { [baseHandlerName]: fn };
102
+
40
103
  return {
41
104
  ...targets,
42
- [target.replace(/\{type\}/, this.type)]: (...args) =>
43
- handler.fn.apply(this, args),
105
+ ...newHandlers,
44
106
  };
45
107
  }, {}),
46
108
  };
@@ -51,24 +113,27 @@ export default class {
51
113
  }
52
114
 
53
115
  @register("{type}Connection")
54
- handleConnection(root, vars) {
116
+ handleConnection(root, vars, _, { fieldName }) {
55
117
  let records = this.filter.filter(
56
- this.collection,
57
- this.serializer.deserialize(vars)
118
+ this.collection.all().models,
119
+ deserialize(vars)
58
120
  );
59
121
 
60
- const relKey = `${camelize(this.type)}Ids`;
122
+ const relKey = `${singularize(fieldName)}Ids`;
61
123
  if (root && Object.prototype.hasOwnProperty.call(root, relKey)) {
62
124
  const ids = root[relKey];
63
- records = records.filter(({ id }) => ids && ids.includes(id));
125
+ records = records
126
+ .filter(({ id }) => ids && ids.includes(id))
127
+ .sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
64
128
  }
65
129
 
66
130
  // add base64 encoded index as cursor to records
67
131
  records = records.map((record, index) => ({
68
- ...record,
132
+ ...record.toJSON(),
69
133
  _cursor: btoa(index),
70
134
  }));
71
135
 
136
+ const totalCount = records.length;
72
137
  const lastCursor = records.slice(-1)[0]?._cursor;
73
138
 
74
139
  // extract next page of records
@@ -86,71 +151,78 @@ export default class {
86
151
  pageInfo: () => {
87
152
  return { hasNextPage, endCursor };
88
153
  },
154
+ totalCount,
89
155
  edges: () =>
90
156
  new MockList(records.length, () => ({
91
157
  node: (r, v, _, meta) =>
92
- this.serializer.serialize(records[meta.path.prev.key]),
158
+ serialize(records[meta.path.prev.key], this.type),
93
159
  })),
94
160
  };
95
161
  }
96
162
 
97
- @register("{type}")
98
- handle(root, vars) {
163
+ @register("{subtype}{type}")
164
+ handle(root, vars, _, { fieldName }) {
99
165
  // If the parent node already resolved this branch in the graph, return it
100
166
  // directly without mocking it
101
167
  if (
102
168
  root &&
103
- Object.prototype.hasOwnProperty.call(root, camelize(this.type))
169
+ fieldName !== "node" &&
170
+ Object.prototype.hasOwnProperty.call(root, fieldName)
104
171
  ) {
105
- return root[camelize(this.type)];
172
+ return root[fieldName];
106
173
  }
107
174
 
108
175
  // If the parent node provides an ID for this relation, filter our mock data
109
176
  // with that given ID
177
+ const relKey = `${fieldName}Id`;
110
178
  if (
111
179
  root &&
112
- Object.prototype.hasOwnProperty.call(root, `${camelize(this.type)}Id`)
180
+ fieldName !== "node" &&
181
+ Object.prototype.hasOwnProperty.call(root, relKey)
113
182
  ) {
114
- vars = { id: root[`${camelize(this.type)}Id`] };
183
+ vars = { id: root[relKey] };
115
184
  }
116
185
 
117
- const record = this.filter.find(
118
- this.collection,
119
- this.serializer.deserialize(vars)
120
- );
121
-
122
- /* istanbul ignore next */
123
- if (!record) {
124
- // eslint-disable-next-line no-console
125
- return Error(
126
- `Did not find a record of type "${this.type}" in the store. Did you forget to create one?`
127
- );
128
- }
186
+ const record = this.collection.findBy(deserialize(vars));
129
187
 
130
- return this.serializer.serialize(record);
188
+ return record && serialize(record.toJSON(), this.type);
131
189
  }
132
190
 
133
- @register("Save{type}Payload")
134
- handleSavePayload(_, { input: { clientMutationId, slug, id, ...args } }) {
191
+ @register("Save{subtype}{type}Payload")
192
+ handleSavePayload(
193
+ _,
194
+ { input: { clientMutationId = faker.datatype.uuid(), slug, id, ...args } }
195
+ ) {
135
196
  const identifier = slug ? { slug } : { id };
136
197
 
137
- const obj = this.filter.find(this.collection, identifier);
198
+ const relKeys = this.schema.modelFor(camelize(this.type)).foreignKeys;
199
+
200
+ const parsedArgs = Object.entries(args).reduce((parsed, [key, value]) => {
201
+ const re = new RegExp(`${camelize(key)}Id(s)?`);
202
+ const relKey = relKeys.find((k) => re.test(k));
203
+
204
+ return {
205
+ ...parsed,
206
+ ...(value === undefined ? {} : { [relKey ?? key]: value }),
207
+ };
208
+ }, {});
209
+
210
+ const obj = this.collection.findBy(identifier);
138
211
  const res = obj
139
- ? this.collection.update(obj.id, args)
140
- : this.collection.insert(
141
- this.serializer.deserialize(
142
- this.server.build(dasherize(this.type), {
212
+ ? obj.update(deserialize(parsedArgs))
213
+ : this.collection.create(
214
+ this.server.build(
215
+ dasherize(this.type),
216
+ deserialize({
143
217
  ...identifier,
144
- ...args,
218
+ ...parsedArgs,
145
219
  })
146
220
  )
147
221
  );
148
222
 
149
- const x = {
150
- [camelize(this.type)]: this.serializer.serialize(res),
223
+ return {
224
+ [camelize(this.type)]: serialize(res.toJSON(), this.type),
151
225
  clientMutationId,
152
226
  };
153
-
154
- return x;
155
227
  }
156
228
  }