templateConfiguration.test.ts 10.4 KB
Newer Older
1
import { TemplateConfiguration } from "./templateConfiguration";
2
3
4
5
6
import {
  FallbackTemplateDefinition,
  TemplateDefinition,
  TemplateOutput,
} from "../types";
7
8
9
import { Webpage } from "../webpage/webpage";
import * as nodeFetch from "node-fetch";
import { pages } from "../webpage/samplePages";
Diegodlh's avatar
Diegodlh committed
10
11
import log from "loglevel";
import { ContentRevision } from "../mediawiki/revisions";
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

const mockNodeFetch = nodeFetch as typeof import("../../__mocks__/node-fetch");

const domain = "example.com";
const targetPath = "/article1";
const targetUrl = "https://" + domain + targetPath;
const nonApplicableTemplate: TemplateDefinition = {
  path: "/template1",
  label: "first template",
  fields: [
    {
      fieldname: "itemType",
      required: true,
      procedures: [
        {
          selections: [
            {
              type: "citoid",
              config: "itemType",
            },
          ],
          transformations: [
            {
              // a split transformation should render the template non applicable
              // because the itemType field is not supposed to be an array
              type: "split",
              config: "",
              itemwise: true,
            },
          ],
        },
      ],
    },
  ],
};
const applicableTemplate: TemplateDefinition = {
  path: "/template2",
  label: "second template",
  fields: [
    {
      fieldname: "title",
      required: true,
      procedures: [
        {
          selections: [
            {
              type: "citoid",
              config: "title",
            },
          ],
          transformations: [],
        },
      ],
    },
  ],
};
const targetTemplate: TemplateDefinition = {
  path: targetPath,
  label: "target template",
  fields: [
    {
      fieldname: "authorLast",
      required: true,
      procedures: [
        {
          selections: [
            {
              type: "citoid",
              config: "authorLast",
            },
          ],
          transformations: [],
        },
      ],
    },
  ],
};

describe("Use an applicable template", () => {
  const templates = [nonApplicableTemplate, applicableTemplate];
92
  const paths = templates.map((template) => template.path);
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
  const configuration = new TemplateConfiguration(
    domain,
    [],
    undefined,
    templates
  );
  const target = new Webpage(targetUrl);

  beforeAll(() => {
    mockNodeFetch.__addCitoidResponse(
      targetUrl,
      JSON.stringify(pages[targetUrl].citoid)
    );
  });

  it("returns an applicable output", async () => {
109
110
111
    const output = (
      await configuration.translateWith(target, paths)
    )[0] as TemplateOutput;
112
113
114
    expect(output.applicable).toBe(true);
  });

115
  it("skips non-applicable templates by default", async () => {
116
117
118
    const output = (
      await configuration.translateWith(target, paths)
    )[0] as TemplateOutput;
119
120
121
    expect(output.template.label).toBe("second template");
  });

122
123
124
125
126
127
128
129
130
  it("optionally returns non-applicable template outputs", async () => {
    const outputs = (await configuration.translateWith(target, paths, {
      onlyApplicable: false,
    })) as TemplateOutput[];
    expect(outputs.length).toBe(2);
    expect(outputs[0].applicable).toBe(false);
    expect(outputs[1].applicable).toBe(true);
  });

131
  it("outputs the expected results", async () => {
132
133
134
    const output = (
      await configuration.translateWith(target, paths)
    )[0] as TemplateOutput;
135
136
137
138
139
140
141
142
143
144
145
146
147
    expect(
      output.outputs.map((field) => [field.fieldname, field.output])
    ).toEqual([["title", ["Sample article"]]]);
  });

  it("fetches the target only once", async () => {
    // create new target and spy on its citoid cache's getData and fetchData methods
    const target = new Webpage(targetUrl);
    const getDataSpy = jest.spyOn(target.cache.citoid, "getData");
    const fetchDataSpy = jest.spyOn(target.cache.citoid, "fetchData");

    const fetchSpy = jest.spyOn(mockNodeFetch, "default");

148
    await configuration.translateWith(target, paths);
149
150
151
152
153
154
155
156
157
158
159
160

    // the citoid cache's getData method should have been called twice
    // once for the citoid selection step in the first template, and
    // once for the citoid selection step in the second template
    expect(getDataSpy).toHaveBeenCalledTimes(2);
    // wheras the citoid cache's fetchData method and node-fetch
    // should have been called only once
    expect(fetchDataSpy).toHaveBeenCalledTimes(1);
    expect(fetchSpy).toHaveBeenCalledTimes(1);
  });

  it("prefers a template for the same path as the target", async () => {
161
162
163
164
165
166
167
168
169
170
171
172
    const configurationWithTargetTemplate = new TemplateConfiguration(
      domain,
      [],
      undefined,
      [...templates, targetTemplate]
    );
    const output = (
      await configurationWithTargetTemplate.translateWith(target, [
        ...paths,
        targetTemplate.path,
      ])
    )[0] as TemplateOutput;
173
174
175
176
177
178
    expect(output.template.label).toBe("target template");
  });
});

describe("Use the fallback template", () => {
  const templates = [nonApplicableTemplate];
179
180
  const paths = templates.map((template) => template.path);
  const fallbackDef: FallbackTemplateDefinition = {
181
182
183
    fields: [
      {
        fieldname: "authorFirst",
184
185
186
187
188
189
190
191
192
193
194
        procedures: [
          {
            selections: [
              {
                type: "citoid",
                config: "authorFirst",
              },
            ],
            transformations: [],
          },
        ],
195
196
197
198
        required: true,
      },
    ],
  };
199
200
201
202
203
204
  const configuration = new TemplateConfiguration(
    domain,
    [],
    fallbackDef,
    templates
  );
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
  const target = new Webpage(targetUrl);

  beforeAll(() => {
    mockNodeFetch.__addCitoidResponse(
      targetUrl,
      JSON.stringify(pages[targetUrl].citoid)
    );
  });

  it("refuses to use fallback template with path", () => {
    const fallbackDefWithPath = {
      ...fallbackDef,
      path: "/some-path",
    };
    expect(() => {
220
      new TemplateConfiguration(domain, [], fallbackDefWithPath, templates);
221
222
223
224
    }).toThrow("should not have template path");
  });

  it("returns an applicable output", async () => {
225
226
227
    const output = (
      await configuration.translateWith(target, paths)
    )[0] as TemplateOutput;
228
229
230
231
    expect(output.applicable).toBe(true);
  });

  it("outputs the expected results", async () => {
232
233
234
    const output = (
      await configuration.translateWith(target, paths)
    )[0] as TemplateOutput;
235
236
237
238
239
240
241
    expect(
      output.outputs.map((field) => [field.fieldname, field.output])
    ).toEqual([["authorFirst", ["John", "Jane"]]]);
  });
});

describe("No applicable templates", () => {
242
243
244
245
246
247
248
249
  const templates = [nonApplicableTemplate];
  const paths = templates.map((template) => template.path);
  const configuration = new TemplateConfiguration(
    domain,
    [],
    undefined,
    templates
  );
250
251
252
253
254
255
256
257
258
  const target = new Webpage(targetUrl);
  beforeAll(() => {
    mockNodeFetch.__addCitoidResponse(
      targetUrl,
      JSON.stringify(pages[targetUrl].citoid)
    );
  });

  it("translation returns false", () => {
259
260
    return expect(configuration.translateWith(target, paths)).resolves.toEqual(
      []
261
    );
262
263
264
  });
});

265
it("multiple templates for the same path are silently ignored", () => {
266
267
268
269
  const duplicatePathTemplate: TemplateDefinition = {
    ...applicableTemplate,
  };
  const templates = [applicableTemplate, duplicatePathTemplate];
270
271
272
273
274
275
276
  const configuration = new TemplateConfiguration(
    domain,
    [],
    undefined,
    templates
  );
  expect(configuration.get().length).toBe(1);
277
});
Diegodlh's avatar
Diegodlh committed
278
279
280

describe("Configuration revisions", () => {
  const warnSpy = jest.spyOn(log, "warn").mockImplementation();
281
282
283
  beforeEach(() => {
    jest.clearAllMocks();
  });
Diegodlh's avatar
Diegodlh committed
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
  it("skips misformatted elements individually", () => {
    const content = JSON.stringify([
      {
        path: "/",
        fields: [
          {
            fieldname: "itemType",
            required: true,
            procedures: [
              {
                selections: [
                  {
                    type: "citoid",
                    config: "itemType",
                  },
                  {
                    type: "invalidType",
                    config: "itemType",
                  },
                ],
                transformations: [
                  {
                    type: "range",
307
                    config: "1",
Diegodlh's avatar
Diegodlh committed
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
                    itemwise: true,
                  },
                  {
                    type: "range",
                    config: "invalidConfig",
                    itemwise: true,
                  },
                ],
              },
              {
                selections: [],
              },
              {
                transformations: [],
              },
            ],
          },
          {
            fieldname: "invalidField",
            required: true,
            procedures: [],
          },
        ],
      },
    ]);
    const revision: ContentRevision = {
      revid: 0,
      timestamp: "",
      content,
    };
    const configuration = new TemplateConfiguration(
      "example.com",
      [],
      undefined
    );
    configuration.loadRevision(revision);
    expect(warnSpy).toHaveBeenCalledTimes(5);
    expect(configuration.get().map((template) => template.toJSON())).toEqual([
      {
        path: "/",
        label: "",
        fields: [
          {
            fieldname: "itemType",
            required: true,
            procedures: [
              {
                selections: [
                  {
                    type: "citoid",
                    config: "itemType",
                  },
                ],
                transformations: [
                  {
                    type: "range",
364
                    config: "1",
Diegodlh's avatar
Diegodlh committed
365
366
367
368
369
370
371
372
373
374
                    itemwise: true,
                  },
                ],
              },
            ],
          },
        ],
      },
    ]);
  });
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
  it("skips templates with missing mandatory fields", () => {
    const definitions: TemplateDefinition[] = [
      {
        path: "/",
        fields: [
          {
            fieldname: "itemType",
            required: true,
            procedures: [],
          },
        ],
      },
    ];
    const revision: ContentRevision = {
      revid: 0,
      timestamp: "",
      content: JSON.stringify(definitions),
    };
    const configuration = new TemplateConfiguration(
      "example.com",
      ["itemType", "title"],
      undefined
    );
    configuration.loadRevision(revision);
    expect(warnSpy).toHaveBeenCalledTimes(1);
    expect(configuration.get().length).toBe(0);
  });
Diegodlh's avatar
Diegodlh committed
402
});