@adobe/spacecat-shared-tokowaka-client 1.2.2 → 1.2.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
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.2.3](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.2.2...@adobe/spacecat-shared-tokowaka-client-v1.2.3) (2025-11-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* readability opportunity handling in edge APIs ([#1188](https://github.com/adobe/spacecat-shared/issues/1188)) ([fdf1bd9](https://github.com/adobe/spacecat-shared/commit/fdf1bd9678e5284200e3fd4378391e515ee8baba))
|
|
7
|
+
|
|
1
8
|
# [@adobe/spacecat-shared-tokowaka-client-v1.2.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.2.1...@adobe/spacecat-shared-tokowaka-client-v1.2.2) (2025-11-28)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -173,6 +173,23 @@ export class HeadingsMapper extends BaseOpportunityMapper {
|
|
|
173
173
|
canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Readability opportunity mapper
|
|
178
|
+
* Handles conversion of readability suggestions to Tokowaka patches
|
|
179
|
+
*/
|
|
180
|
+
export class ReadabilityMapper extends BaseOpportunityMapper {
|
|
181
|
+
constructor(log: any);
|
|
182
|
+
|
|
183
|
+
getOpportunityType(): string;
|
|
184
|
+
requiresPrerender(): boolean;
|
|
185
|
+
suggestionsToPatches(
|
|
186
|
+
urlPath: string,
|
|
187
|
+
suggestions: Suggestion[],
|
|
188
|
+
opportunityId: string
|
|
189
|
+
): TokawakaPatch[];
|
|
190
|
+
canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
/**
|
|
177
194
|
* Content summarization opportunity mapper
|
|
178
195
|
* Handles conversion of content summarization suggestions to Tokowaka patches with HAST format
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import HeadingsMapper from './headings-mapper.js';
|
|
14
14
|
import ContentSummarizationMapper from './content-summarization-mapper.js';
|
|
15
15
|
import FaqMapper from './faq-mapper.js';
|
|
16
|
+
import ReadabilityMapper from './readability-mapper.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Registry for opportunity mappers
|
|
@@ -34,6 +35,7 @@ export default class MapperRegistry {
|
|
|
34
35
|
HeadingsMapper,
|
|
35
36
|
ContentSummarizationMapper,
|
|
36
37
|
FaqMapper,
|
|
38
|
+
ReadabilityMapper,
|
|
37
39
|
// more mappers here
|
|
38
40
|
];
|
|
39
41
|
|
|
@@ -0,0 +1,113 @@
|
|
|
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 { hasText, isValidUrl, isBoolean } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
15
|
+
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mapper for readability opportunity
|
|
19
|
+
* Handles conversion of readability suggestions to Tokowaka patches
|
|
20
|
+
*/
|
|
21
|
+
export default class ReadabilityMapper extends BaseOpportunityMapper {
|
|
22
|
+
constructor(log) {
|
|
23
|
+
super(log);
|
|
24
|
+
this.opportunityType = 'readability';
|
|
25
|
+
this.prerenderRequired = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getOpportunityType() {
|
|
29
|
+
return this.opportunityType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
requiresPrerender() {
|
|
33
|
+
return this.prerenderRequired;
|
|
34
|
+
}
|
|
35
|
+
|
|
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 = [];
|
|
45
|
+
|
|
46
|
+
suggestions.forEach((suggestion) => {
|
|
47
|
+
const eligibility = this.canDeploy(suggestion);
|
|
48
|
+
if (!eligibility.eligible) {
|
|
49
|
+
this.log.warn(`Readability suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = suggestion.getData();
|
|
54
|
+
const { transformRules } = data;
|
|
55
|
+
|
|
56
|
+
const patch = {
|
|
57
|
+
...this.createBasePatch(suggestion, opportunityId),
|
|
58
|
+
op: transformRules.op,
|
|
59
|
+
selector: transformRules.selector,
|
|
60
|
+
value: transformRules.value,
|
|
61
|
+
valueFormat: 'text',
|
|
62
|
+
...(
|
|
63
|
+
isBoolean(transformRules.prerenderRequired)
|
|
64
|
+
&& { prerenderRequired: transformRules.prerenderRequired }
|
|
65
|
+
),
|
|
66
|
+
...(hasText(data.textPreview) && { currValue: data.textPreview }),
|
|
67
|
+
target: transformRules.target || TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
patches.push(patch);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return patches;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a readability suggestion can be deployed
|
|
78
|
+
* @param {Object} suggestion - Suggestion object
|
|
79
|
+
* @returns {Object} { eligible: boolean, reason?: string }
|
|
80
|
+
*/
|
|
81
|
+
// eslint-disable-next-line class-methods-use-this
|
|
82
|
+
canDeploy(suggestion) {
|
|
83
|
+
const data = suggestion.getData();
|
|
84
|
+
|
|
85
|
+
// Validate required fields
|
|
86
|
+
if (!data?.transformRules) {
|
|
87
|
+
return { eligible: false, reason: 'transformRules is required' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { transformRules } = data;
|
|
91
|
+
|
|
92
|
+
if (!isValidUrl(data.pageUrl)) {
|
|
93
|
+
return { eligible: false, reason: `pageUrl ${data.pageUrl} is not a valid URL` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!hasText(transformRules.selector)) {
|
|
97
|
+
return { eligible: false, reason: 'transformRules.selector is required' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (transformRules.op !== 'replace') {
|
|
101
|
+
return {
|
|
102
|
+
eligible: false,
|
|
103
|
+
reason: 'transformRules.op must be "replace" for readability suggestions',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!hasText(transformRules.value)) {
|
|
108
|
+
return { eligible: false, reason: 'transformRules.value is required' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { eligible: true };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
/* eslint-env mocha */
|
|
14
|
+
|
|
15
|
+
import { expect } from 'chai';
|
|
16
|
+
import ReadabilityMapper from '../../src/mappers/readability-mapper.js';
|
|
17
|
+
|
|
18
|
+
describe('ReadabilityMapper', () => {
|
|
19
|
+
let mapper;
|
|
20
|
+
let log;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
log = {
|
|
24
|
+
debug: () => {},
|
|
25
|
+
info: () => {},
|
|
26
|
+
warn: () => {},
|
|
27
|
+
error: () => {},
|
|
28
|
+
};
|
|
29
|
+
mapper = new ReadabilityMapper(log);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getOpportunityType', () => {
|
|
33
|
+
it('should return readability', () => {
|
|
34
|
+
expect(mapper.getOpportunityType()).to.equal('readability');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('requiresPrerender', () => {
|
|
39
|
+
it('should return true', () => {
|
|
40
|
+
expect(mapper.requiresPrerender()).to.be.true;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('canDeploy', () => {
|
|
45
|
+
it('should return eligible for valid readability suggestion', () => {
|
|
46
|
+
const suggestion = {
|
|
47
|
+
getData: () => ({
|
|
48
|
+
textPreview: 'Lorem ipsum...',
|
|
49
|
+
pageUrl: 'https://www.website.com',
|
|
50
|
+
transformRules: {
|
|
51
|
+
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
52
|
+
op: 'replace',
|
|
53
|
+
selector: '#main-238828 > div:nth-child(1) > span',
|
|
54
|
+
target: 'ai-bots',
|
|
55
|
+
prerenderRequired: true,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result = mapper.canDeploy(suggestion);
|
|
61
|
+
|
|
62
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return eligible for readability suggestion without target (uses default)', () => {
|
|
66
|
+
const suggestion = {
|
|
67
|
+
getData: () => ({
|
|
68
|
+
textPreview: 'Lorem ipsum...',
|
|
69
|
+
pageUrl: 'https://www.website.com',
|
|
70
|
+
transformRules: {
|
|
71
|
+
value: 'Improved readability text...',
|
|
72
|
+
op: 'replace',
|
|
73
|
+
selector: '#content > p',
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = mapper.canDeploy(suggestion);
|
|
79
|
+
|
|
80
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return ineligible when transformRules is missing', () => {
|
|
84
|
+
const suggestion = {
|
|
85
|
+
getData: () => ({
|
|
86
|
+
textPreview: 'Lorem ipsum...',
|
|
87
|
+
pageUrl: 'https://www.website.com',
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = mapper.canDeploy(suggestion);
|
|
92
|
+
|
|
93
|
+
expect(result).to.deep.equal({
|
|
94
|
+
eligible: false,
|
|
95
|
+
reason: 'transformRules is required',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return ineligible when data is null', () => {
|
|
100
|
+
const suggestion = {
|
|
101
|
+
getData: () => null,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = mapper.canDeploy(suggestion);
|
|
105
|
+
|
|
106
|
+
expect(result).to.deep.equal({
|
|
107
|
+
eligible: false,
|
|
108
|
+
reason: 'transformRules is required',
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return ineligible when transformRules.selector is missing', () => {
|
|
113
|
+
const suggestion = {
|
|
114
|
+
getData: () => ({
|
|
115
|
+
textPreview: 'Text...',
|
|
116
|
+
pageUrl: 'https://www.example.com',
|
|
117
|
+
transformRules: {
|
|
118
|
+
value: 'New text',
|
|
119
|
+
op: 'replace',
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = mapper.canDeploy(suggestion);
|
|
125
|
+
|
|
126
|
+
expect(result).to.deep.equal({
|
|
127
|
+
eligible: false,
|
|
128
|
+
reason: 'transformRules.selector is required',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return ineligible when transformRules.op is missing', () => {
|
|
133
|
+
const suggestion = {
|
|
134
|
+
getData: () => ({
|
|
135
|
+
textPreview: 'Text...',
|
|
136
|
+
pageUrl: 'https://www.example.com',
|
|
137
|
+
transformRules: {
|
|
138
|
+
value: 'New text',
|
|
139
|
+
selector: '#content',
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const result = mapper.canDeploy(suggestion);
|
|
145
|
+
|
|
146
|
+
expect(result).to.deep.equal({
|
|
147
|
+
eligible: false,
|
|
148
|
+
reason: 'transformRules.op must be "replace" for readability suggestions',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should return ineligible when transformRules.op is not "replace"', () => {
|
|
153
|
+
const suggestion = {
|
|
154
|
+
getData: () => ({
|
|
155
|
+
textPreview: 'Text...',
|
|
156
|
+
pageUrl: 'https://www.example.com',
|
|
157
|
+
transformRules: {
|
|
158
|
+
value: 'New text',
|
|
159
|
+
op: 'insertAfter',
|
|
160
|
+
selector: '#content',
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = mapper.canDeploy(suggestion);
|
|
166
|
+
|
|
167
|
+
expect(result).to.deep.equal({
|
|
168
|
+
eligible: false,
|
|
169
|
+
reason: 'transformRules.op must be "replace" for readability suggestions',
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should return ineligible when transformRules.value is missing', () => {
|
|
174
|
+
const suggestion = {
|
|
175
|
+
getData: () => ({
|
|
176
|
+
textPreview: 'Text...',
|
|
177
|
+
pageUrl: 'https://www.example.com',
|
|
178
|
+
transformRules: {
|
|
179
|
+
op: 'replace',
|
|
180
|
+
selector: '#content',
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = mapper.canDeploy(suggestion);
|
|
186
|
+
|
|
187
|
+
expect(result).to.deep.equal({
|
|
188
|
+
eligible: false,
|
|
189
|
+
reason: 'transformRules.value is required',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return ineligible when transformRules.selector is empty string', () => {
|
|
194
|
+
const suggestion = {
|
|
195
|
+
getData: () => ({
|
|
196
|
+
textPreview: 'Text...',
|
|
197
|
+
pageUrl: 'https://www.example.com',
|
|
198
|
+
transformRules: {
|
|
199
|
+
value: 'New text',
|
|
200
|
+
op: 'replace',
|
|
201
|
+
selector: '',
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = mapper.canDeploy(suggestion);
|
|
207
|
+
|
|
208
|
+
expect(result).to.deep.equal({
|
|
209
|
+
eligible: false,
|
|
210
|
+
reason: 'transformRules.selector is required',
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return ineligible when pageUrl is invalid', () => {
|
|
215
|
+
const suggestion = {
|
|
216
|
+
getData: () => ({
|
|
217
|
+
textPreview: 'Text...',
|
|
218
|
+
pageUrl: 'not-a-valid-url',
|
|
219
|
+
transformRules: {
|
|
220
|
+
value: 'New text',
|
|
221
|
+
op: 'replace',
|
|
222
|
+
selector: '#content',
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const result = mapper.canDeploy(suggestion);
|
|
228
|
+
|
|
229
|
+
expect(result).to.deep.equal({
|
|
230
|
+
eligible: false,
|
|
231
|
+
reason: 'pageUrl not-a-valid-url is not a valid URL',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should return ineligible when pageUrl is missing', () => {
|
|
236
|
+
const suggestion = {
|
|
237
|
+
getData: () => ({
|
|
238
|
+
textPreview: 'Text...',
|
|
239
|
+
transformRules: {
|
|
240
|
+
value: 'New text',
|
|
241
|
+
op: 'replace',
|
|
242
|
+
selector: '#content',
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = mapper.canDeploy(suggestion);
|
|
248
|
+
|
|
249
|
+
expect(result).to.deep.equal({
|
|
250
|
+
eligible: false,
|
|
251
|
+
reason: 'pageUrl undefined is not a valid URL',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('suggestionsToPatches', () => {
|
|
257
|
+
it('should create patch for readability suggestion', () => {
|
|
258
|
+
const suggestion = {
|
|
259
|
+
getId: () => 'sugg-123',
|
|
260
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
261
|
+
getData: () => ({
|
|
262
|
+
textPreview: 'Lorem ipsum...',
|
|
263
|
+
pageUrl: 'https://www.website.com',
|
|
264
|
+
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
265
|
+
transformRules: {
|
|
266
|
+
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
267
|
+
op: 'replace',
|
|
268
|
+
selector: '#main-238828 > div:nth-child(1) > span',
|
|
269
|
+
target: 'ai-bots',
|
|
270
|
+
prerenderRequired: true,
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-123');
|
|
276
|
+
expect(patches.length).to.equal(1);
|
|
277
|
+
const patch = patches[0];
|
|
278
|
+
|
|
279
|
+
expect(patch).to.deep.include({
|
|
280
|
+
op: 'replace',
|
|
281
|
+
selector: '#main-238828 > div:nth-child(1) > span',
|
|
282
|
+
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
283
|
+
valueFormat: 'text',
|
|
284
|
+
currValue: 'Lorem ipsum...',
|
|
285
|
+
target: 'ai-bots',
|
|
286
|
+
opportunityId: 'opp-123',
|
|
287
|
+
suggestionId: 'sugg-123',
|
|
288
|
+
prerenderRequired: true,
|
|
289
|
+
});
|
|
290
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should create patch with scrapedAt timestamp', () => {
|
|
294
|
+
const suggestion = {
|
|
295
|
+
getId: () => 'sugg-456',
|
|
296
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
297
|
+
getData: () => ({
|
|
298
|
+
textPreview: 'Original text...',
|
|
299
|
+
pageUrl: 'https://www.example.com',
|
|
300
|
+
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
301
|
+
transformRules: {
|
|
302
|
+
value: 'Improved readability text',
|
|
303
|
+
op: 'replace',
|
|
304
|
+
selector: '.content > p',
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-456');
|
|
310
|
+
expect(patches.length).to.equal(1);
|
|
311
|
+
const patch = patches[0];
|
|
312
|
+
|
|
313
|
+
expect(patch.lastUpdated).to.equal(new Date('2025-09-20T06:21:12.584Z').getTime());
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should use default target when not specified', () => {
|
|
317
|
+
const suggestion = {
|
|
318
|
+
getId: () => 'sugg-789',
|
|
319
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
320
|
+
getData: () => ({
|
|
321
|
+
textPreview: 'Text...',
|
|
322
|
+
pageUrl: 'https://www.example.com',
|
|
323
|
+
transformRules: {
|
|
324
|
+
value: 'Better text',
|
|
325
|
+
op: 'replace',
|
|
326
|
+
selector: '#content',
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-789');
|
|
332
|
+
expect(patches.length).to.equal(1);
|
|
333
|
+
const patch = patches[0];
|
|
334
|
+
|
|
335
|
+
expect(patch.target).to.equal('ai-bots');
|
|
336
|
+
expect(patch.currValue).to.equal('Text...');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should return empty array for invalid suggestion', () => {
|
|
340
|
+
const suggestion = {
|
|
341
|
+
getId: () => 'sugg-999',
|
|
342
|
+
getData: () => ({
|
|
343
|
+
textPreview: 'Text...',
|
|
344
|
+
pageUrl: 'https://www.example.com',
|
|
345
|
+
// Missing transformRules
|
|
346
|
+
}),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-999');
|
|
350
|
+
expect(patches.length).to.equal(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should log warning for invalid suggestion', () => {
|
|
354
|
+
let warnMessage = '';
|
|
355
|
+
const warnLog = {
|
|
356
|
+
debug: () => {},
|
|
357
|
+
info: () => {},
|
|
358
|
+
warn: (msg) => { warnMessage = msg; },
|
|
359
|
+
error: () => {},
|
|
360
|
+
};
|
|
361
|
+
const warnMapper = new ReadabilityMapper(warnLog);
|
|
362
|
+
|
|
363
|
+
const suggestion = {
|
|
364
|
+
getId: () => 'sugg-warn',
|
|
365
|
+
getData: () => ({
|
|
366
|
+
textPreview: 'Text...',
|
|
367
|
+
pageUrl: 'https://www.example.com',
|
|
368
|
+
transformRules: {
|
|
369
|
+
op: 'replace',
|
|
370
|
+
// Missing selector and value
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const patches = warnMapper.suggestionsToPatches('/path', [suggestion], 'opp-warn');
|
|
376
|
+
|
|
377
|
+
expect(patches.length).to.equal(0);
|
|
378
|
+
expect(warnMessage).to.include('cannot be deployed');
|
|
379
|
+
expect(warnMessage).to.include('sugg-warn');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle multiple suggestions', () => {
|
|
383
|
+
const suggestions = [
|
|
384
|
+
{
|
|
385
|
+
getId: () => 'sugg-1',
|
|
386
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
387
|
+
getData: () => ({
|
|
388
|
+
textPreview: 'Original text 1',
|
|
389
|
+
pageUrl: 'https://www.example.com/page1',
|
|
390
|
+
transformRules: {
|
|
391
|
+
value: 'First improved text',
|
|
392
|
+
op: 'replace',
|
|
393
|
+
selector: '#p1',
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
getId: () => 'sugg-2',
|
|
399
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
400
|
+
getData: () => ({
|
|
401
|
+
textPreview: 'Original text 2',
|
|
402
|
+
pageUrl: 'https://www.example.com/page2',
|
|
403
|
+
transformRules: {
|
|
404
|
+
value: 'Second improved text',
|
|
405
|
+
op: 'replace',
|
|
406
|
+
selector: '#p2',
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
const patches = mapper.suggestionsToPatches('/path', suggestions, 'opp-multi');
|
|
413
|
+
expect(patches.length).to.equal(2);
|
|
414
|
+
expect(patches[0].suggestionId).to.equal('sugg-1');
|
|
415
|
+
expect(patches[0].selector).to.equal('#p1');
|
|
416
|
+
expect(patches[0].currValue).to.equal('Original text 1');
|
|
417
|
+
expect(patches[1].suggestionId).to.equal('sugg-2');
|
|
418
|
+
expect(patches[1].selector).to.equal('#p2');
|
|
419
|
+
expect(patches[1].currValue).to.equal('Original text 2');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|