@projectcaluma/ember-testing 9.1.0 → 10.2.0-beta.2

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 (50) hide show
  1. package/CHANGELOG.md +994 -0
  2. package/addon/mirage-graphql/filters/answer.js +13 -0
  3. package/addon/mirage-graphql/filters/base.js +47 -20
  4. package/addon/mirage-graphql/filters/question.js +1 -3
  5. package/addon/mirage-graphql/filters/work-item.js +22 -2
  6. package/addon/mirage-graphql/handler.js +25 -7
  7. package/addon/mirage-graphql/index.js +24 -3
  8. package/addon/mirage-graphql/mocks/answer.js +8 -33
  9. package/addon/mirage-graphql/mocks/base.js +105 -42
  10. package/addon/mirage-graphql/mocks/form.js +25 -100
  11. package/addon/mirage-graphql/mocks/question.js +17 -119
  12. package/addon/mirage-graphql/mocks/work-item.js +105 -0
  13. package/addon/mirage-graphql/schema.graphql +0 -16
  14. package/addon/scenarios/distribution.js +315 -0
  15. package/addon-mirage-support/factories/answer.js +4 -2
  16. package/addon-mirage-support/factories/case.js +3 -2
  17. package/addon-mirage-support/factories/document.js +5 -2
  18. package/addon-mirage-support/factories/file.js +3 -2
  19. package/addon-mirage-support/factories/form.js +5 -2
  20. package/addon-mirage-support/factories/format-validator.js +5 -2
  21. package/addon-mirage-support/factories/option.js +4 -1
  22. package/addon-mirage-support/factories/question.js +6 -2
  23. package/addon-mirage-support/factories/task.js +6 -5
  24. package/addon-mirage-support/factories/work-item.js +3 -2
  25. package/addon-mirage-support/factories/workflow.js +9 -0
  26. package/addon-mirage-support/models/answer.js +3 -2
  27. package/addon-mirage-support/models/case.js +2 -1
  28. package/addon-mirage-support/models/document.js +1 -1
  29. package/addon-mirage-support/models/file.js +1 -1
  30. package/addon-mirage-support/models/form.js +2 -2
  31. package/addon-mirage-support/models/format-validator.js +1 -1
  32. package/addon-mirage-support/models/option.js +1 -1
  33. package/addon-mirage-support/models/question.js +5 -3
  34. package/addon-mirage-support/models/task.js +1 -1
  35. package/addon-mirage-support/models/work-item.js +3 -1
  36. package/addon-mirage-support/models/workflow.js +1 -1
  37. package/index.js +1 -1
  38. package/package.json +15 -13
  39. package/addon/mirage-graphql/mocks/case.js +0 -9
  40. package/addon/mirage-graphql/mocks/task.js +0 -55
  41. package/addon/mirage-graphql/resolvers/index.js +0 -16
  42. package/addon/mirage-graphql/serializers/answer.js +0 -14
  43. package/addon/mirage-graphql/serializers/base.js +0 -13
  44. package/addon/mirage-graphql/serializers/case.js +0 -11
  45. package/addon/mirage-graphql/serializers/document.js +0 -11
  46. package/addon/mirage-graphql/serializers/file.js +0 -12
  47. package/addon/mirage-graphql/serializers/form.js +0 -11
  48. package/addon/mirage-graphql/serializers/question.js +0 -14
  49. package/addon/mirage-graphql/serializers/task.js +0 -16
  50. 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
  }
@@ -1,7 +1,27 @@
1
1
  import BaseFilter from "@projectcaluma/ember-testing/mirage-graphql/filters/base";
2
2
 
3
3
  export default class extends BaseFilter {
4
- status(records, value) {
5
- return records.filter(({ status }) => status === value);
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
+ );
6
26
  }
7
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 faker from "@faker-js/faker";
3
+ import { singularize, pluralize } from "ember-inflector";
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,21 +113,23 @@ 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
 
@@ -91,46 +155,47 @@ export default class {
91
155
  edges: () =>
92
156
  new MockList(records.length, () => ({
93
157
  node: (r, v, _, meta) =>
94
- this.serializer.serialize(records[meta.path.prev.key]),
158
+ serialize(records[meta.path.prev.key], this.type),
95
159
  })),
96
160
  };
97
161
  }
98
162
 
99
- @register("{type}")
100
- handle(root, vars) {
163
+ @register("{subtype}{type}")
164
+ handle(root, vars, _, { fieldName }) {
101
165
  // If the parent node already resolved this branch in the graph, return it
102
166
  // directly without mocking it
103
167
  if (
104
168
  root &&
105
- Object.prototype.hasOwnProperty.call(root, camelize(this.type))
169
+ fieldName !== "node" &&
170
+ Object.prototype.hasOwnProperty.call(root, fieldName)
106
171
  ) {
107
- return root[camelize(this.type)];
172
+ return root[fieldName];
108
173
  }
109
174
 
110
175
  // If the parent node provides an ID for this relation, filter our mock data
111
176
  // with that given ID
177
+ const relKey = `${fieldName}Id`;
112
178
  if (
113
179
  root &&
114
- Object.prototype.hasOwnProperty.call(root, `${camelize(this.type)}Id`)
180
+ fieldName !== "node" &&
181
+ Object.prototype.hasOwnProperty.call(root, relKey)
115
182
  ) {
116
- vars = { id: root[`${camelize(this.type)}Id`] };
183
+ vars = { id: root[relKey] };
117
184
  }
118
185
 
119
- const record = this.filter.find(
120
- this.collection,
121
- this.serializer.deserialize(vars)
122
- );
186
+ const record = this.collection.findBy(deserialize(vars));
123
187
 
124
- return record && this.serializer.serialize(record);
188
+ return record && serialize(record.toJSON(), this.type);
125
189
  }
126
190
 
127
- @register("Save{type}Payload")
128
- 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
+ ) {
129
196
  const identifier = slug ? { slug } : { id };
130
197
 
131
- const relKeys = this.server.schema.modelFor(
132
- this.type.toLowerCase()
133
- ).foreignKeys;
198
+ const relKeys = this.schema.modelFor(camelize(this.type)).foreignKeys;
134
199
 
135
200
  const parsedArgs = Object.entries(args).reduce((parsed, [key, value]) => {
136
201
  const re = new RegExp(`${camelize(key)}Id(s)?`);
@@ -138,16 +203,17 @@ export default class {
138
203
 
139
204
  return {
140
205
  ...parsed,
141
- [relKey ?? key]: value,
206
+ ...(value === undefined ? {} : { [relKey ?? key]: value }),
142
207
  };
143
208
  }, {});
144
209
 
145
- const obj = this.filter.find(this.collection, identifier);
210
+ const obj = this.collection.findBy(identifier);
146
211
  const res = obj
147
- ? this.collection.update(obj.id, parsedArgs)
148
- : this.collection.insert(
149
- this.serializer.deserialize(
150
- 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({
151
217
  ...identifier,
152
218
  ...parsedArgs,
153
219
  })
@@ -155,10 +221,7 @@ export default class {
155
221
  );
156
222
 
157
223
  return {
158
- [camelize(this.type)]: this.serializer.serialize({
159
- ...relKeys.reduce((rels, key) => ({ ...rels, [key]: null })),
160
- ...res,
161
- }),
224
+ [camelize(this.type)]: serialize(res.toJSON(), this.type),
162
225
  clientMutationId,
163
226
  };
164
227
  }