@adobe/spacecat-shared-tokowaka-client 1.0.2 → 1.0.3
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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/index.d.ts +39 -10
- package/src/index.js +41 -87
- package/src/mappers/base-mapper.js +19 -9
- package/src/mappers/content-summarization-mapper.js +38 -35
- package/src/mappers/faq-mapper.js +204 -0
- package/src/mappers/headings-mapper.js +37 -23
- package/src/mappers/mapper-registry.js +2 -0
- package/src/utils/markdown-utils.js +24 -0
- package/src/utils/patch-utils.js +66 -0
- package/src/utils/s3-utils.js +20 -0
- package/src/utils/site-utils.js +25 -0
- package/src/utils/suggestion-utils.js +69 -0
- package/test/index.test.js +113 -7
- package/test/mappers/base-mapper.test.js +107 -7
- package/test/mappers/content-mapper.test.js +26 -24
- package/test/mappers/faq-mapper.test.js +1324 -0
- package/test/mappers/faq-mapper.test.js.backup +1264 -0
- package/test/mappers/headings-mapper.test.js +25 -17
- package/test/utils/patch-utils.test.js +148 -0
|
@@ -33,30 +33,44 @@ export default class HeadingsMapper extends BaseOpportunityMapper {
|
|
|
33
33
|
return this.prerenderRequired;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Converts suggestions to Tokowaka patches
|
|
38
|
+
* @param {string} urlPath - URL path for the suggestions
|
|
39
|
+
* @param {Array} suggestions - Array of suggestion entities for the same URL
|
|
40
|
+
* @param {string} opportunityId - Opportunity ID
|
|
41
|
+
* @returns {Array} - Array of Tokowaka patch objects
|
|
42
|
+
*/
|
|
43
|
+
suggestionsToPatches(urlPath, suggestions, opportunityId) {
|
|
44
|
+
const patches = [];
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
suggestions.forEach((suggestion) => {
|
|
47
|
+
const eligibility = this.canDeploy(suggestion);
|
|
48
|
+
if (!eligibility.eligible) {
|
|
49
|
+
this.log.warn(`Headings suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = suggestion.getData();
|
|
54
|
+
const { checkType, transformRules } = data;
|
|
55
|
+
|
|
56
|
+
const patch = {
|
|
57
|
+
...this.createBasePatch(suggestion, opportunityId),
|
|
58
|
+
op: transformRules.action,
|
|
59
|
+
selector: transformRules.selector,
|
|
60
|
+
value: data.recommendedAction,
|
|
61
|
+
valueFormat: 'text',
|
|
62
|
+
...(data.currentValue !== null && { currValue: data.currentValue }),
|
|
63
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (checkType === 'heading-missing-h1' && transformRules.tag) {
|
|
67
|
+
patch.tag = transformRules.tag;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
patches.push(patch);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return patches;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import HeadingsMapper from './headings-mapper.js';
|
|
14
14
|
import ContentSummarizationMapper from './content-summarization-mapper.js';
|
|
15
|
+
import FaqMapper from './faq-mapper.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Registry for opportunity mappers
|
|
@@ -32,6 +33,7 @@ export default class MapperRegistry {
|
|
|
32
33
|
const defaultMappers = [
|
|
33
34
|
HeadingsMapper,
|
|
34
35
|
ContentSummarizationMapper,
|
|
36
|
+
FaqMapper,
|
|
35
37
|
// more mappers here
|
|
36
38
|
];
|
|
37
39
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { toHast } from 'mdast-util-to-hast';
|
|
14
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts markdown text to HAST (Hypertext Abstract Syntax Tree) format
|
|
18
|
+
* @param {string} markdown - Markdown text
|
|
19
|
+
* @returns {Object} - HAST object
|
|
20
|
+
*/
|
|
21
|
+
export function markdownToHast(markdown) {
|
|
22
|
+
const mdast = fromMarkdown(markdown);
|
|
23
|
+
return toHast(mdast);
|
|
24
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates a unique key for a patch based on its structure
|
|
15
|
+
* Individual patches (one suggestion per patch):
|
|
16
|
+
* → Key: opportunityId:suggestionId
|
|
17
|
+
* Patches with no suggestionId:
|
|
18
|
+
* → Key: opportunityId
|
|
19
|
+
*/
|
|
20
|
+
function getPatchKey(patch) {
|
|
21
|
+
// Heading patch (no suggestionId): use special key
|
|
22
|
+
if (!patch.suggestionId) {
|
|
23
|
+
return `${patch.opportunityId}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Individual patches include suggestionId in key
|
|
27
|
+
// This ensures each suggestion gets its own separate patch
|
|
28
|
+
return `${patch.opportunityId}:${patch.suggestionId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merges new patches into existing patches based on patch keys
|
|
33
|
+
* - If a patch with the same key exists, it's updated
|
|
34
|
+
* - If a patch with a new key is found, it's added
|
|
35
|
+
* @param {Array} existingPatches - Array of existing patches
|
|
36
|
+
* @param {Array} newPatches - Array of new patches to merge
|
|
37
|
+
* @returns {Object} - { patches: Array, updateCount: number, addCount: number }
|
|
38
|
+
*/
|
|
39
|
+
export function mergePatches(existingPatches, newPatches) {
|
|
40
|
+
// Create a map of existing patches by their key
|
|
41
|
+
const patchMap = new Map();
|
|
42
|
+
existingPatches.forEach((patch, index) => {
|
|
43
|
+
const key = getPatchKey(patch);
|
|
44
|
+
patchMap.set(key, { patch, index });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Process new patches
|
|
48
|
+
const mergedPatches = [...existingPatches];
|
|
49
|
+
let updateCount = 0;
|
|
50
|
+
let addCount = 0;
|
|
51
|
+
|
|
52
|
+
newPatches.forEach((newPatch) => {
|
|
53
|
+
const key = getPatchKey(newPatch);
|
|
54
|
+
const existing = patchMap.get(key);
|
|
55
|
+
|
|
56
|
+
if (existing) {
|
|
57
|
+
mergedPatches[existing.index] = newPatch;
|
|
58
|
+
updateCount += 1;
|
|
59
|
+
} else {
|
|
60
|
+
mergedPatches.push(newPatch);
|
|
61
|
+
addCount += 1;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { patches: mergedPatches, updateCount, addCount };
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates S3 path for Tokowaka configuration
|
|
15
|
+
* @param {string} apiKey - Tokowaka API key
|
|
16
|
+
* @returns {string} - S3 path
|
|
17
|
+
*/
|
|
18
|
+
export function getTokowakaConfigS3Path(apiKey) {
|
|
19
|
+
return `opportunities/${apiKey}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidUrl } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets the effective base URL for a site, respecting overrideBaseURL if configured
|
|
17
|
+
* @param {Object} site - Site entity
|
|
18
|
+
* @returns {string} - Base URL to use
|
|
19
|
+
*/
|
|
20
|
+
export function getEffectiveBaseURL(site) {
|
|
21
|
+
const overrideBaseURL = site.getConfig()?.getFetchConfig?.()?.overrideBaseURL;
|
|
22
|
+
return (overrideBaseURL && isValidUrl(overrideBaseURL))
|
|
23
|
+
? overrideBaseURL
|
|
24
|
+
: site.getBaseURL();
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Groups suggestions by URL pathname
|
|
15
|
+
* @param {Array} suggestions - Array of suggestion entities
|
|
16
|
+
* @param {string} baseURL - Base URL for pathname extraction
|
|
17
|
+
* @param {Object} log - Logger instance
|
|
18
|
+
* @returns {Object} - Object with URL paths as keys and arrays of suggestions as values
|
|
19
|
+
*/
|
|
20
|
+
export function groupSuggestionsByUrlPath(suggestions, baseURL, log) {
|
|
21
|
+
return suggestions.reduce((acc, suggestion) => {
|
|
22
|
+
const data = suggestion.getData();
|
|
23
|
+
const url = data?.url;
|
|
24
|
+
|
|
25
|
+
if (!url) {
|
|
26
|
+
log.warn(`Suggestion ${suggestion.getId()} does not have a URL, skipping`);
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let urlPath;
|
|
31
|
+
try {
|
|
32
|
+
urlPath = new URL(url, baseURL).pathname;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
log.warn(`Failed to extract pathname from URL for suggestion ${suggestion.getId()}: ${url}`);
|
|
35
|
+
return acc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!acc[urlPath]) {
|
|
39
|
+
acc[urlPath] = [];
|
|
40
|
+
}
|
|
41
|
+
acc[urlPath].push(suggestion);
|
|
42
|
+
return acc;
|
|
43
|
+
}, {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Filters suggestions into eligible and ineligible based on mapper's canDeploy method
|
|
48
|
+
* @param {Array} suggestions - Array of suggestion entities
|
|
49
|
+
* @param {Object} mapper - Mapper instance with canDeploy method
|
|
50
|
+
* @returns {Object} - { eligible: Array, ineligible: Array<{suggestion, reason}> }
|
|
51
|
+
*/
|
|
52
|
+
export function filterEligibleSuggestions(suggestions, mapper) {
|
|
53
|
+
const eligible = [];
|
|
54
|
+
const ineligible = [];
|
|
55
|
+
|
|
56
|
+
suggestions.forEach((suggestion) => {
|
|
57
|
+
const eligibility = mapper.canDeploy(suggestion);
|
|
58
|
+
if (eligibility.eligible) {
|
|
59
|
+
eligible.push(suggestion);
|
|
60
|
+
} else {
|
|
61
|
+
ineligible.push({
|
|
62
|
+
suggestion,
|
|
63
|
+
reason: eligibility.reason || 'Suggestion cannot be deployed',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { eligible, ineligible };
|
|
69
|
+
}
|
package/test/index.test.js
CHANGED
|
@@ -199,7 +199,7 @@ describe('TokowakaClient', () => {
|
|
|
199
199
|
|
|
200
200
|
describe('generateConfig', () => {
|
|
201
201
|
it('should generate config for headings opportunity', () => {
|
|
202
|
-
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions);
|
|
202
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
203
203
|
|
|
204
204
|
expect(config).to.deep.include({
|
|
205
205
|
siteId: 'site-123',
|
|
@@ -218,12 +218,105 @@ describe('TokowakaClient', () => {
|
|
|
218
218
|
selector: 'h1',
|
|
219
219
|
value: 'New Heading',
|
|
220
220
|
opportunityId: 'opp-123',
|
|
221
|
-
suggestionId: 'sugg-1',
|
|
222
221
|
prerenderRequired: true,
|
|
223
222
|
});
|
|
223
|
+
expect(patch.suggestionId).to.equal('sugg-1');
|
|
224
224
|
expect(patch).to.have.property('lastUpdated');
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
it('should generate config for FAQ opportunity', () => {
|
|
228
|
+
mockOpportunity = {
|
|
229
|
+
getId: () => 'opp-faq-123',
|
|
230
|
+
getType: () => 'faq',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
mockSuggestions = [
|
|
234
|
+
{
|
|
235
|
+
getId: () => 'sugg-faq-1',
|
|
236
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
237
|
+
getData: () => ({
|
|
238
|
+
url: 'https://example.com/page1',
|
|
239
|
+
headingText: 'FAQs',
|
|
240
|
+
shouldOptimize: true,
|
|
241
|
+
item: {
|
|
242
|
+
question: 'Question 1?',
|
|
243
|
+
answer: 'Answer 1.',
|
|
244
|
+
},
|
|
245
|
+
transformRules: {
|
|
246
|
+
action: 'appendChild',
|
|
247
|
+
selector: 'main',
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
getId: () => 'sugg-faq-2',
|
|
253
|
+
getUpdatedAt: () => '2025-01-15T11:00:00.000Z',
|
|
254
|
+
getData: () => ({
|
|
255
|
+
url: 'https://example.com/page1',
|
|
256
|
+
headingText: 'FAQs',
|
|
257
|
+
shouldOptimize: true,
|
|
258
|
+
item: {
|
|
259
|
+
question: 'Question 2?',
|
|
260
|
+
answer: 'Answer 2.',
|
|
261
|
+
},
|
|
262
|
+
transformRules: {
|
|
263
|
+
action: 'appendChild',
|
|
264
|
+
selector: 'main',
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
271
|
+
|
|
272
|
+
expect(config).to.deep.include({
|
|
273
|
+
siteId: 'site-123',
|
|
274
|
+
baseURL: 'https://example.com',
|
|
275
|
+
version: '1.0',
|
|
276
|
+
tokowakaForceFail: false,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(config.tokowakaOptimizations).to.have.property('/page1');
|
|
280
|
+
expect(config.tokowakaOptimizations['/page1'].prerender).to.be.true;
|
|
281
|
+
expect(config.tokowakaOptimizations['/page1'].patches).to.have.length(3); // heading + 2 FAQs
|
|
282
|
+
|
|
283
|
+
// First patch: heading (no suggestionId)
|
|
284
|
+
const headingPatch = config.tokowakaOptimizations['/page1'].patches[0];
|
|
285
|
+
expect(headingPatch).to.include({
|
|
286
|
+
op: 'appendChild',
|
|
287
|
+
selector: 'main',
|
|
288
|
+
opportunityId: 'opp-faq-123',
|
|
289
|
+
prerenderRequired: true,
|
|
290
|
+
});
|
|
291
|
+
expect(headingPatch.suggestionId).to.be.undefined;
|
|
292
|
+
expect(headingPatch).to.have.property('lastUpdated');
|
|
293
|
+
expect(headingPatch.value.tagName).to.equal('h2');
|
|
294
|
+
|
|
295
|
+
// Second patch: first FAQ
|
|
296
|
+
const firstFaqPatch = config.tokowakaOptimizations['/page1'].patches[1];
|
|
297
|
+
expect(firstFaqPatch).to.include({
|
|
298
|
+
op: 'appendChild',
|
|
299
|
+
selector: 'main',
|
|
300
|
+
opportunityId: 'opp-faq-123',
|
|
301
|
+
prerenderRequired: true,
|
|
302
|
+
});
|
|
303
|
+
expect(firstFaqPatch.suggestionId).to.equal('sugg-faq-1');
|
|
304
|
+
expect(firstFaqPatch).to.have.property('lastUpdated');
|
|
305
|
+
expect(firstFaqPatch.value.tagName).to.equal('div');
|
|
306
|
+
|
|
307
|
+
// Third patch: second FAQ
|
|
308
|
+
const secondFaqPatch = config.tokowakaOptimizations['/page1'].patches[2];
|
|
309
|
+
expect(secondFaqPatch).to.include({
|
|
310
|
+
op: 'appendChild',
|
|
311
|
+
selector: 'main',
|
|
312
|
+
opportunityId: 'opp-faq-123',
|
|
313
|
+
prerenderRequired: true,
|
|
314
|
+
});
|
|
315
|
+
expect(secondFaqPatch.suggestionId).to.equal('sugg-faq-2');
|
|
316
|
+
expect(secondFaqPatch).to.have.property('lastUpdated');
|
|
317
|
+
expect(secondFaqPatch.value.tagName).to.equal('div');
|
|
318
|
+
});
|
|
319
|
+
|
|
227
320
|
it('should group suggestions by URL path', () => {
|
|
228
321
|
mockSuggestions = [
|
|
229
322
|
{
|
|
@@ -254,7 +347,7 @@ describe('TokowakaClient', () => {
|
|
|
254
347
|
},
|
|
255
348
|
];
|
|
256
349
|
|
|
257
|
-
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions);
|
|
350
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
258
351
|
|
|
259
352
|
expect(Object.keys(config.tokowakaOptimizations)).to.have.length(2);
|
|
260
353
|
expect(config.tokowakaOptimizations).to.have.property('/page1');
|
|
@@ -289,7 +382,7 @@ describe('TokowakaClient', () => {
|
|
|
289
382
|
},
|
|
290
383
|
];
|
|
291
384
|
|
|
292
|
-
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions);
|
|
385
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
293
386
|
|
|
294
387
|
expect(Object.keys(config.tokowakaOptimizations)).to.have.length(1);
|
|
295
388
|
expect(config.tokowakaOptimizations).to.have.property('/relative-path');
|
|
@@ -306,7 +399,7 @@ describe('TokowakaClient', () => {
|
|
|
306
399
|
},
|
|
307
400
|
];
|
|
308
401
|
|
|
309
|
-
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions);
|
|
402
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
310
403
|
|
|
311
404
|
expect(Object.keys(config.tokowakaOptimizations)).to.have.length(0);
|
|
312
405
|
expect(log.warn).to.have.been.calledWith(sinon.match(/does not have a URL/));
|
|
@@ -329,7 +422,7 @@ describe('TokowakaClient', () => {
|
|
|
329
422
|
},
|
|
330
423
|
];
|
|
331
424
|
|
|
332
|
-
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions);
|
|
425
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
333
426
|
|
|
334
427
|
expect(Object.keys(config.tokowakaOptimizations)).to.have.length(0);
|
|
335
428
|
expect(log.warn).to.have.been.calledWith(sinon.match(/Failed to extract pathname from URL/));
|
|
@@ -363,10 +456,23 @@ describe('TokowakaClient', () => {
|
|
|
363
456
|
},
|
|
364
457
|
];
|
|
365
458
|
|
|
366
|
-
expect(() => client.generateConfig(mockSite, mockOpportunity, mockSuggestions))
|
|
459
|
+
expect(() => client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null))
|
|
367
460
|
.to.throw(/No mapper found for opportunity type: unsupported-type/)
|
|
368
461
|
.with.property('status', 501);
|
|
369
462
|
});
|
|
463
|
+
|
|
464
|
+
it('should generate config without allOpportunitySuggestions', () => {
|
|
465
|
+
const config = client.generateConfig(mockSite, mockOpportunity, mockSuggestions, null);
|
|
466
|
+
|
|
467
|
+
expect(config).to.deep.include({
|
|
468
|
+
siteId: 'site-123',
|
|
469
|
+
baseURL: 'https://example.com',
|
|
470
|
+
version: '1.0',
|
|
471
|
+
tokowakaForceFail: false,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(config.tokowakaOptimizations).to.have.property('/page1');
|
|
475
|
+
});
|
|
370
476
|
});
|
|
371
477
|
|
|
372
478
|
describe('uploadConfig', () => {
|
|
@@ -41,9 +41,9 @@ describe('BaseOpportunityMapper', () => {
|
|
|
41
41
|
.to.throw('requiresPrerender() must be implemented by subclass');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('
|
|
45
|
-
expect(() => mapper.
|
|
46
|
-
.to.throw('
|
|
44
|
+
it('suggestionsToPatches should throw error', () => {
|
|
45
|
+
expect(() => mapper.suggestionsToPatches('/path', [], 'opp-123', null))
|
|
46
|
+
.to.throw('suggestionsToPatches() must be implemented by subclass');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('canDeploy should throw error if not implemented', () => {
|
|
@@ -60,14 +60,15 @@ describe('BaseOpportunityMapper', () => {
|
|
|
60
60
|
|
|
61
61
|
requiresPrerender() { return true; }
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
suggestionsToPatches() { return []; }
|
|
64
64
|
|
|
65
65
|
canDeploy() { return { eligible: true }; }
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const testMapper = new TestMapper();
|
|
68
|
+
const testMapper = new TestMapper(log);
|
|
69
69
|
const suggestion = {
|
|
70
70
|
getId: () => 'test-123',
|
|
71
|
+
getData: () => ({}),
|
|
71
72
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
72
73
|
};
|
|
73
74
|
|
|
@@ -85,14 +86,15 @@ describe('BaseOpportunityMapper', () => {
|
|
|
85
86
|
|
|
86
87
|
requiresPrerender() { return true; }
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
suggestionsToPatches() { return []; }
|
|
89
90
|
|
|
90
91
|
canDeploy() { return { eligible: true }; }
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const testMapper = new TestMapper();
|
|
94
|
+
const testMapper = new TestMapper(log);
|
|
94
95
|
const suggestion = {
|
|
95
96
|
getId: () => 'test-no-date',
|
|
97
|
+
getData: () => ({}),
|
|
96
98
|
getUpdatedAt: () => null, // Returns null
|
|
97
99
|
};
|
|
98
100
|
|
|
@@ -106,5 +108,103 @@ describe('BaseOpportunityMapper', () => {
|
|
|
106
108
|
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
107
109
|
expect(patch.prerenderRequired).to.be.true;
|
|
108
110
|
});
|
|
111
|
+
|
|
112
|
+
it('should prioritize scrapedAt from getData()', () => {
|
|
113
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
114
|
+
getOpportunityType() { return 'test'; }
|
|
115
|
+
|
|
116
|
+
requiresPrerender() { return true; }
|
|
117
|
+
|
|
118
|
+
suggestionsToPatches() { return []; }
|
|
119
|
+
|
|
120
|
+
canDeploy() { return { eligible: true }; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const testMapper = new TestMapper(log);
|
|
124
|
+
const scrapedTime = '2025-01-20T15:30:00.000Z';
|
|
125
|
+
const suggestion = {
|
|
126
|
+
getId: () => 'test-scraped',
|
|
127
|
+
getData: () => ({ scrapedAt: scrapedTime }),
|
|
128
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-scraped');
|
|
132
|
+
|
|
133
|
+
expect(patch.lastUpdated).to.equal(new Date(scrapedTime).getTime());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use transformRules.scrapedAt when scrapedAt is not available', () => {
|
|
137
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
138
|
+
getOpportunityType() { return 'test'; }
|
|
139
|
+
|
|
140
|
+
requiresPrerender() { return true; }
|
|
141
|
+
|
|
142
|
+
suggestionsToPatches() { return []; }
|
|
143
|
+
|
|
144
|
+
canDeploy() { return { eligible: true }; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const testMapper = new TestMapper(log);
|
|
148
|
+
const transformScrapedTime = '2025-01-18T12:00:00.000Z';
|
|
149
|
+
const suggestion = {
|
|
150
|
+
getId: () => 'test-transform',
|
|
151
|
+
getData: () => ({ transformRules: { scrapedAt: transformScrapedTime } }),
|
|
152
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-transform');
|
|
156
|
+
|
|
157
|
+
expect(patch.lastUpdated).to.equal(new Date(transformScrapedTime).getTime());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle invalid date strings by using Date.now()', () => {
|
|
161
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
162
|
+
getOpportunityType() { return 'test'; }
|
|
163
|
+
|
|
164
|
+
requiresPrerender() { return true; }
|
|
165
|
+
|
|
166
|
+
suggestionsToPatches() { return []; }
|
|
167
|
+
|
|
168
|
+
canDeploy() { return { eligible: true }; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const testMapper = new TestMapper(log);
|
|
172
|
+
const suggestion = {
|
|
173
|
+
getId: () => 'test-invalid',
|
|
174
|
+
getData: () => ({}),
|
|
175
|
+
getUpdatedAt: () => 'invalid-date-string',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const beforeTime = Date.now();
|
|
179
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-invalid');
|
|
180
|
+
const afterTime = Date.now();
|
|
181
|
+
|
|
182
|
+
// Should fallback to Date.now() for invalid dates
|
|
183
|
+
expect(patch.lastUpdated).to.be.at.least(beforeTime);
|
|
184
|
+
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle missing getData() gracefully', () => {
|
|
188
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
189
|
+
getOpportunityType() { return 'test'; }
|
|
190
|
+
|
|
191
|
+
requiresPrerender() { return true; }
|
|
192
|
+
|
|
193
|
+
suggestionsToPatches() { return []; }
|
|
194
|
+
|
|
195
|
+
canDeploy() { return { eligible: true }; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const testMapper = new TestMapper(log);
|
|
199
|
+
const suggestion = {
|
|
200
|
+
getId: () => 'test-no-data',
|
|
201
|
+
getData: () => null,
|
|
202
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-no-data');
|
|
206
|
+
|
|
207
|
+
expect(patch.lastUpdated).to.equal(new Date('2025-01-15T10:00:00.000Z').getTime());
|
|
208
|
+
});
|
|
109
209
|
});
|
|
110
210
|
});
|