@adobe/spacecat-shared-tokowaka-client 1.2.3 → 1.3.0
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 +14 -0
- package/package.json +1 -1
- package/src/mappers/generic-mapper.js +117 -0
- package/src/mappers/mapper-registry.js +2 -0
- package/src/mappers/readability-mapper.js +2 -2
- package/test/mappers/generic-mapper.test.js +665 -0
- package/test/mappers/readability-mapper.test.js +20 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.3.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.2.4...@adobe/spacecat-shared-tokowaka-client-v1.3.0) (2025-12-04)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Tokowaka generic oppty mapper ([#1204](https://github.com/adobe/spacecat-shared/issues/1204)) ([710a36c](https://github.com/adobe/spacecat-shared/commit/710a36cd8b4e6f91a702da252c5a8739444a4e46))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.2.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.2.3...@adobe/spacecat-shared-tokowaka-client-v1.2.4) (2025-11-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* url param in tokowaka readability mapper ([#1192](https://github.com/adobe/spacecat-shared/issues/1192)) ([566b9f1](https://github.com/adobe/spacecat-shared/commit/566b9f1d9ca03a9ffe6bbf59a89d6ccba8b278c0))
|
|
14
|
+
|
|
1
15
|
# [@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
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
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 } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
15
|
+
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generic mapper for code change opportunities
|
|
19
|
+
* Handles conversion of generic code change suggestions to Tokowaka patches
|
|
20
|
+
*/
|
|
21
|
+
export default class GenericMapper extends BaseOpportunityMapper {
|
|
22
|
+
constructor(log) {
|
|
23
|
+
super(log);
|
|
24
|
+
this.opportunityType = 'generic-autofix-edge';
|
|
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(`Generic 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.action,
|
|
59
|
+
selector: transformRules.selector,
|
|
60
|
+
value: data.patchValue,
|
|
61
|
+
valueFormat: data.format || 'text',
|
|
62
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (data.tag) {
|
|
66
|
+
patch.tag = data.tag;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
patches.push(patch);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return patches;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks if a generic suggestion can be deployed
|
|
77
|
+
* @param {Object} suggestion - Suggestion object
|
|
78
|
+
* @returns {Object} { eligible: boolean, reason?: string }
|
|
79
|
+
*/
|
|
80
|
+
// eslint-disable-next-line class-methods-use-this
|
|
81
|
+
canDeploy(suggestion) {
|
|
82
|
+
const data = suggestion.getData();
|
|
83
|
+
|
|
84
|
+
// Validate transformRules exists
|
|
85
|
+
if (!data?.transformRules) {
|
|
86
|
+
return { eligible: false, reason: 'transformRules is required' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate required fields
|
|
90
|
+
if (!hasText(data.transformRules.selector)) {
|
|
91
|
+
return { eligible: false, reason: 'transformRules.selector is required' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!hasText(data?.patchValue)) {
|
|
95
|
+
return { eligible: false, reason: 'patchValue is required' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!hasText(data.transformRules.action)) {
|
|
99
|
+
return { eligible: false, reason: 'transformRules.action is required' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate action value
|
|
103
|
+
const validOperations = ['insertBefore', 'insertAfter', 'replace'];
|
|
104
|
+
if (!validOperations.includes(data.transformRules.action)) {
|
|
105
|
+
return {
|
|
106
|
+
eligible: false,
|
|
107
|
+
reason: `transformRules.action must be one of: ${validOperations.join(', ')}. Got: ${data.transformRules.action}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!hasText(data?.url)) {
|
|
112
|
+
return { eligible: false, reason: 'url is required' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { eligible: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -14,6 +14,7 @@ import HeadingsMapper from './headings-mapper.js';
|
|
|
14
14
|
import ContentSummarizationMapper from './content-summarization-mapper.js';
|
|
15
15
|
import FaqMapper from './faq-mapper.js';
|
|
16
16
|
import ReadabilityMapper from './readability-mapper.js';
|
|
17
|
+
import GenericMapper from './generic-mapper.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Registry for opportunity mappers
|
|
@@ -36,6 +37,7 @@ export default class MapperRegistry {
|
|
|
36
37
|
ContentSummarizationMapper,
|
|
37
38
|
FaqMapper,
|
|
38
39
|
ReadabilityMapper,
|
|
40
|
+
GenericMapper,
|
|
39
41
|
// more mappers here
|
|
40
42
|
];
|
|
41
43
|
|
|
@@ -89,8 +89,8 @@ export default class ReadabilityMapper extends BaseOpportunityMapper {
|
|
|
89
89
|
|
|
90
90
|
const { transformRules } = data;
|
|
91
91
|
|
|
92
|
-
if (!isValidUrl(data.
|
|
93
|
-
return { eligible: false, reason: `
|
|
92
|
+
if (!isValidUrl(data.url)) {
|
|
93
|
+
return { eligible: false, reason: `url ${data.url} is not a valid URL` };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (!hasText(transformRules.selector)) {
|
|
@@ -0,0 +1,665 @@
|
|
|
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 GenericMapper from '../../src/mappers/generic-mapper.js';
|
|
17
|
+
|
|
18
|
+
describe('GenericMapper', () => {
|
|
19
|
+
let mapper;
|
|
20
|
+
let log;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
log = {
|
|
24
|
+
debug: () => {},
|
|
25
|
+
info: () => {},
|
|
26
|
+
warn: () => {},
|
|
27
|
+
error: () => {},
|
|
28
|
+
};
|
|
29
|
+
mapper = new GenericMapper(log);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getOpportunityType', () => {
|
|
33
|
+
it('should return generic', () => {
|
|
34
|
+
expect(mapper.getOpportunityType()).to.equal('generic-autofix-edge');
|
|
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 suggestion with all required fields', () => {
|
|
46
|
+
const suggestion = {
|
|
47
|
+
getData: () => ({
|
|
48
|
+
transformRules: {
|
|
49
|
+
action: 'insertAfter',
|
|
50
|
+
selector: '#create-with-multiple-top-ai-models-all-in-one-place',
|
|
51
|
+
},
|
|
52
|
+
patchValue: 'Blah Blah some text',
|
|
53
|
+
url: 'https://www.adobe.com/products/firefly.html',
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = mapper.canDeploy(suggestion);
|
|
58
|
+
|
|
59
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return eligible for insertBefore operation', () => {
|
|
63
|
+
const suggestion = {
|
|
64
|
+
getData: () => ({
|
|
65
|
+
transformRules: {
|
|
66
|
+
action: 'insertBefore',
|
|
67
|
+
selector: 'h1',
|
|
68
|
+
},
|
|
69
|
+
patchValue: 'New content',
|
|
70
|
+
url: 'https://example.com/page',
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = mapper.canDeploy(suggestion);
|
|
75
|
+
|
|
76
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return eligible for replace operation', () => {
|
|
80
|
+
const suggestion = {
|
|
81
|
+
getData: () => ({
|
|
82
|
+
transformRules: {
|
|
83
|
+
action: 'replace',
|
|
84
|
+
selector: '.content',
|
|
85
|
+
},
|
|
86
|
+
patchValue: 'Replaced content',
|
|
87
|
+
url: 'https://example.com/page',
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = mapper.canDeploy(suggestion);
|
|
92
|
+
|
|
93
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return ineligible when transformRules is missing', () => {
|
|
97
|
+
const suggestion = {
|
|
98
|
+
getData: () => ({
|
|
99
|
+
patchValue: 'Some text',
|
|
100
|
+
url: 'https://example.com/page',
|
|
101
|
+
}),
|
|
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 selector is missing', () => {
|
|
113
|
+
const suggestion = {
|
|
114
|
+
getData: () => ({
|
|
115
|
+
transformRules: {
|
|
116
|
+
action: 'insertAfter',
|
|
117
|
+
},
|
|
118
|
+
patchValue: 'Some text',
|
|
119
|
+
url: 'https://example.com/page',
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = mapper.canDeploy(suggestion);
|
|
124
|
+
|
|
125
|
+
expect(result).to.deep.equal({
|
|
126
|
+
eligible: false,
|
|
127
|
+
reason: 'transformRules.selector is required',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return ineligible when selector is empty string', () => {
|
|
132
|
+
const suggestion = {
|
|
133
|
+
getData: () => ({
|
|
134
|
+
transformRules: {
|
|
135
|
+
action: 'insertAfter',
|
|
136
|
+
selector: '',
|
|
137
|
+
},
|
|
138
|
+
patchValue: 'Some text',
|
|
139
|
+
url: 'https://example.com/page',
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = mapper.canDeploy(suggestion);
|
|
144
|
+
|
|
145
|
+
expect(result).to.deep.equal({
|
|
146
|
+
eligible: false,
|
|
147
|
+
reason: 'transformRules.selector is required',
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return ineligible when patchValue is missing', () => {
|
|
152
|
+
const suggestion = {
|
|
153
|
+
getData: () => ({
|
|
154
|
+
transformRules: {
|
|
155
|
+
action: 'insertAfter',
|
|
156
|
+
selector: '#selector',
|
|
157
|
+
},
|
|
158
|
+
url: 'https://example.com/page',
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = mapper.canDeploy(suggestion);
|
|
163
|
+
|
|
164
|
+
expect(result).to.deep.equal({
|
|
165
|
+
eligible: false,
|
|
166
|
+
reason: 'patchValue is required',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return ineligible when patchValue is empty string', () => {
|
|
171
|
+
const suggestion = {
|
|
172
|
+
getData: () => ({
|
|
173
|
+
transformRules: {
|
|
174
|
+
action: 'insertAfter',
|
|
175
|
+
selector: '#selector',
|
|
176
|
+
},
|
|
177
|
+
patchValue: '',
|
|
178
|
+
url: 'https://example.com/page',
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = mapper.canDeploy(suggestion);
|
|
183
|
+
|
|
184
|
+
expect(result).to.deep.equal({
|
|
185
|
+
eligible: false,
|
|
186
|
+
reason: 'patchValue is required',
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return ineligible when action is missing', () => {
|
|
191
|
+
const suggestion = {
|
|
192
|
+
getData: () => ({
|
|
193
|
+
transformRules: {
|
|
194
|
+
selector: '#selector',
|
|
195
|
+
},
|
|
196
|
+
patchValue: 'Some text',
|
|
197
|
+
url: 'https://example.com/page',
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = mapper.canDeploy(suggestion);
|
|
202
|
+
|
|
203
|
+
expect(result).to.deep.equal({
|
|
204
|
+
eligible: false,
|
|
205
|
+
reason: 'transformRules.action is required',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should return ineligible when action is empty string', () => {
|
|
210
|
+
const suggestion = {
|
|
211
|
+
getData: () => ({
|
|
212
|
+
transformRules: {
|
|
213
|
+
action: '',
|
|
214
|
+
selector: '#selector',
|
|
215
|
+
},
|
|
216
|
+
patchValue: 'Some text',
|
|
217
|
+
url: 'https://example.com/page',
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = mapper.canDeploy(suggestion);
|
|
222
|
+
|
|
223
|
+
expect(result).to.deep.equal({
|
|
224
|
+
eligible: false,
|
|
225
|
+
reason: 'transformRules.action is required',
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should return ineligible when action is invalid', () => {
|
|
230
|
+
const suggestion = {
|
|
231
|
+
getData: () => ({
|
|
232
|
+
transformRules: {
|
|
233
|
+
action: 'invalidOperation',
|
|
234
|
+
selector: '#selector',
|
|
235
|
+
},
|
|
236
|
+
patchValue: 'Some text',
|
|
237
|
+
url: 'https://example.com/page',
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const result = mapper.canDeploy(suggestion);
|
|
242
|
+
|
|
243
|
+
expect(result).to.deep.equal({
|
|
244
|
+
eligible: false,
|
|
245
|
+
reason: 'transformRules.action must be one of: insertBefore, insertAfter, replace. Got: invalidOperation',
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should return ineligible when url is missing', () => {
|
|
250
|
+
const suggestion = {
|
|
251
|
+
getData: () => ({
|
|
252
|
+
transformRules: {
|
|
253
|
+
action: 'insertAfter',
|
|
254
|
+
selector: '#selector',
|
|
255
|
+
},
|
|
256
|
+
patchValue: 'Some text',
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = mapper.canDeploy(suggestion);
|
|
261
|
+
|
|
262
|
+
expect(result).to.deep.equal({
|
|
263
|
+
eligible: false,
|
|
264
|
+
reason: 'url is required',
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return ineligible when url is empty string', () => {
|
|
269
|
+
const suggestion = {
|
|
270
|
+
getData: () => ({
|
|
271
|
+
transformRules: {
|
|
272
|
+
action: 'insertAfter',
|
|
273
|
+
selector: '#selector',
|
|
274
|
+
},
|
|
275
|
+
patchValue: 'Some text',
|
|
276
|
+
url: '',
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = mapper.canDeploy(suggestion);
|
|
281
|
+
|
|
282
|
+
expect(result).to.deep.equal({
|
|
283
|
+
eligible: false,
|
|
284
|
+
reason: 'url is required',
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should return ineligible when data is null', () => {
|
|
289
|
+
const suggestion = {
|
|
290
|
+
getData: () => null,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const result = mapper.canDeploy(suggestion);
|
|
294
|
+
|
|
295
|
+
expect(result).to.deep.equal({
|
|
296
|
+
eligible: false,
|
|
297
|
+
reason: 'transformRules is required',
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should return ineligible when data is undefined', () => {
|
|
302
|
+
const suggestion = {
|
|
303
|
+
getData: () => undefined,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const result = mapper.canDeploy(suggestion);
|
|
307
|
+
|
|
308
|
+
expect(result).to.deep.equal({
|
|
309
|
+
eligible: false,
|
|
310
|
+
reason: 'transformRules is required',
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('suggestionsToPatches', () => {
|
|
316
|
+
it('should create patch for valid suggestion with insertAfter', () => {
|
|
317
|
+
const suggestion = {
|
|
318
|
+
getId: () => 'ee8fc5e8-29c1-4894-9391-efc10b8a5f5c',
|
|
319
|
+
getUpdatedAt: () => '2025-11-27T16:22:14.258Z',
|
|
320
|
+
getData: () => ({
|
|
321
|
+
transformRules: {
|
|
322
|
+
action: 'insertAfter',
|
|
323
|
+
selector: '#create-with-multiple-top-ai-models-all-in-one-place',
|
|
324
|
+
},
|
|
325
|
+
patchValue: 'Blah Blah some text',
|
|
326
|
+
url: 'https://www.adobe.com/products/firefly.html',
|
|
327
|
+
contentBefore: '**Create with multiple top AI models, all in one place.**',
|
|
328
|
+
rationale: 'This makes LLMs read more text about blah blah.',
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const patches = mapper.suggestionsToPatches(
|
|
333
|
+
'/products/firefly.html',
|
|
334
|
+
[suggestion],
|
|
335
|
+
'7a663e47-e132-4bba-954a-26419e0541b8',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(patches.length).to.equal(1);
|
|
339
|
+
const patch = patches[0];
|
|
340
|
+
|
|
341
|
+
expect(patch).to.deep.include({
|
|
342
|
+
op: 'insertAfter',
|
|
343
|
+
selector: '#create-with-multiple-top-ai-models-all-in-one-place',
|
|
344
|
+
value: 'Blah Blah some text',
|
|
345
|
+
valueFormat: 'text',
|
|
346
|
+
opportunityId: '7a663e47-e132-4bba-954a-26419e0541b8',
|
|
347
|
+
suggestionId: 'ee8fc5e8-29c1-4894-9391-efc10b8a5f5c',
|
|
348
|
+
prerenderRequired: true,
|
|
349
|
+
});
|
|
350
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
351
|
+
expect(patch.target).to.equal('ai-bots');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should create patch for insertBefore operation', () => {
|
|
355
|
+
const suggestion = {
|
|
356
|
+
getId: () => 'sugg-123',
|
|
357
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
358
|
+
getData: () => ({
|
|
359
|
+
transformRules: {
|
|
360
|
+
action: 'insertBefore',
|
|
361
|
+
selector: 'h1',
|
|
362
|
+
},
|
|
363
|
+
patchValue: 'Important notice',
|
|
364
|
+
url: 'https://example.com/page',
|
|
365
|
+
}),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-123');
|
|
369
|
+
|
|
370
|
+
expect(patches.length).to.equal(1);
|
|
371
|
+
const patch = patches[0];
|
|
372
|
+
|
|
373
|
+
expect(patch).to.deep.include({
|
|
374
|
+
op: 'insertBefore',
|
|
375
|
+
selector: 'h1',
|
|
376
|
+
value: 'Important notice',
|
|
377
|
+
valueFormat: 'text',
|
|
378
|
+
opportunityId: 'opp-123',
|
|
379
|
+
suggestionId: 'sugg-123',
|
|
380
|
+
prerenderRequired: true,
|
|
381
|
+
});
|
|
382
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should create patch for replace operation', () => {
|
|
386
|
+
const suggestion = {
|
|
387
|
+
getId: () => 'sugg-456',
|
|
388
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
389
|
+
getData: () => ({
|
|
390
|
+
transformRules: {
|
|
391
|
+
action: 'replace',
|
|
392
|
+
selector: '.content',
|
|
393
|
+
},
|
|
394
|
+
patchValue: 'Replaced content text',
|
|
395
|
+
url: 'https://example.com/page2',
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const patches = mapper.suggestionsToPatches('/page2', [suggestion], 'opp-456');
|
|
400
|
+
|
|
401
|
+
expect(patches.length).to.equal(1);
|
|
402
|
+
const patch = patches[0];
|
|
403
|
+
|
|
404
|
+
expect(patch).to.deep.include({
|
|
405
|
+
op: 'replace',
|
|
406
|
+
selector: '.content',
|
|
407
|
+
value: 'Replaced content text',
|
|
408
|
+
valueFormat: 'text',
|
|
409
|
+
opportunityId: 'opp-456',
|
|
410
|
+
suggestionId: 'sugg-456',
|
|
411
|
+
prerenderRequired: true,
|
|
412
|
+
});
|
|
413
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should handle multiple suggestions', () => {
|
|
417
|
+
const suggestions = [
|
|
418
|
+
{
|
|
419
|
+
getId: () => 'sugg-1',
|
|
420
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
421
|
+
getData: () => ({
|
|
422
|
+
transformRules: {
|
|
423
|
+
action: 'insertAfter',
|
|
424
|
+
selector: '#selector1',
|
|
425
|
+
},
|
|
426
|
+
patchValue: 'Text 1',
|
|
427
|
+
url: 'https://example.com/page',
|
|
428
|
+
}),
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
getId: () => 'sugg-2',
|
|
432
|
+
getUpdatedAt: () => '2025-01-15T11:00:00.000Z',
|
|
433
|
+
getData: () => ({
|
|
434
|
+
transformRules: {
|
|
435
|
+
action: 'insertBefore',
|
|
436
|
+
selector: '#selector2',
|
|
437
|
+
},
|
|
438
|
+
patchValue: 'Text 2',
|
|
439
|
+
url: 'https://example.com/page',
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-123');
|
|
445
|
+
|
|
446
|
+
expect(patches.length).to.equal(2);
|
|
447
|
+
expect(patches[0].suggestionId).to.equal('sugg-1');
|
|
448
|
+
expect(patches[0].value).to.equal('Text 1');
|
|
449
|
+
expect(patches[1].suggestionId).to.equal('sugg-2');
|
|
450
|
+
expect(patches[1].value).to.equal('Text 2');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should return empty array for invalid suggestion', () => {
|
|
454
|
+
const suggestion = {
|
|
455
|
+
getId: () => 'sugg-invalid',
|
|
456
|
+
getData: () => ({
|
|
457
|
+
transformRules: {
|
|
458
|
+
action: 'insertAfter',
|
|
459
|
+
selector: '#selector',
|
|
460
|
+
},
|
|
461
|
+
// Missing patchValue
|
|
462
|
+
url: 'https://example.com/page',
|
|
463
|
+
}),
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-invalid');
|
|
467
|
+
|
|
468
|
+
expect(patches.length).to.equal(0);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should skip invalid suggestions but process valid ones', () => {
|
|
472
|
+
const suggestions = [
|
|
473
|
+
{
|
|
474
|
+
getId: () => 'sugg-valid',
|
|
475
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
476
|
+
getData: () => ({
|
|
477
|
+
transformRules: {
|
|
478
|
+
action: 'insertAfter',
|
|
479
|
+
selector: '#valid',
|
|
480
|
+
},
|
|
481
|
+
patchValue: 'Valid text',
|
|
482
|
+
url: 'https://example.com/page',
|
|
483
|
+
}),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
getId: () => 'sugg-invalid',
|
|
487
|
+
getData: () => ({
|
|
488
|
+
transformRules: {
|
|
489
|
+
action: 'insertAfter',
|
|
490
|
+
selector: '#invalid',
|
|
491
|
+
},
|
|
492
|
+
// Missing patchValue
|
|
493
|
+
url: 'https://example.com/page',
|
|
494
|
+
}),
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-123');
|
|
499
|
+
|
|
500
|
+
expect(patches.length).to.equal(1);
|
|
501
|
+
expect(patches[0].suggestionId).to.equal('sugg-valid');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should log warning for invalid suggestion', () => {
|
|
505
|
+
let warnMessage = '';
|
|
506
|
+
const warnLog = {
|
|
507
|
+
debug: () => {},
|
|
508
|
+
info: () => {},
|
|
509
|
+
warn: (msg) => { warnMessage = msg; },
|
|
510
|
+
error: () => {},
|
|
511
|
+
};
|
|
512
|
+
const warnMapper = new GenericMapper(warnLog);
|
|
513
|
+
|
|
514
|
+
const suggestion = {
|
|
515
|
+
getId: () => 'sugg-warn',
|
|
516
|
+
getData: () => ({
|
|
517
|
+
transformRules: {
|
|
518
|
+
action: 'insertAfter',
|
|
519
|
+
selector: '#selector',
|
|
520
|
+
},
|
|
521
|
+
// Missing patchValue
|
|
522
|
+
url: 'https://example.com/page',
|
|
523
|
+
}),
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const patches = warnMapper.suggestionsToPatches('/page', [suggestion], 'opp-warn');
|
|
527
|
+
|
|
528
|
+
expect(patches.length).to.equal(0);
|
|
529
|
+
expect(warnMessage).to.include('Generic suggestion sugg-warn cannot be deployed');
|
|
530
|
+
expect(warnMessage).to.include('patchValue is required');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should handle complex CSS selectors', () => {
|
|
534
|
+
const suggestion = {
|
|
535
|
+
getId: () => 'sugg-complex',
|
|
536
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
537
|
+
getData: () => ({
|
|
538
|
+
transformRules: {
|
|
539
|
+
action: 'insertAfter',
|
|
540
|
+
selector: '#text-85a9876220 > h2:nth-of-type(1)',
|
|
541
|
+
},
|
|
542
|
+
patchValue: 'Complex selector content',
|
|
543
|
+
url: 'https://example.com/page',
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-complex');
|
|
548
|
+
|
|
549
|
+
expect(patches.length).to.equal(1);
|
|
550
|
+
expect(patches[0].selector).to.equal('#text-85a9876220 > h2:nth-of-type(1)');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should handle multiline patchValue', () => {
|
|
554
|
+
const suggestion = {
|
|
555
|
+
getId: () => 'sugg-multiline',
|
|
556
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
557
|
+
getData: () => ({
|
|
558
|
+
transformRules: {
|
|
559
|
+
action: 'replace',
|
|
560
|
+
selector: '.content',
|
|
561
|
+
},
|
|
562
|
+
patchValue: 'Line 1\nLine 2\nLine 3',
|
|
563
|
+
url: 'https://example.com/page',
|
|
564
|
+
}),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-multiline');
|
|
568
|
+
|
|
569
|
+
expect(patches.length).to.equal(1);
|
|
570
|
+
expect(patches[0].value).to.equal('Line 1\nLine 2\nLine 3');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should include tag when provided', () => {
|
|
574
|
+
const suggestion = {
|
|
575
|
+
getId: () => 'sugg-with-tag',
|
|
576
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
577
|
+
getData: () => ({
|
|
578
|
+
transformRules: {
|
|
579
|
+
action: 'insertAfter',
|
|
580
|
+
selector: '#selector',
|
|
581
|
+
},
|
|
582
|
+
patchValue: 'Content with tag',
|
|
583
|
+
format: 'hast',
|
|
584
|
+
tag: 'div',
|
|
585
|
+
url: 'https://example.com/page',
|
|
586
|
+
}),
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-tag');
|
|
590
|
+
|
|
591
|
+
expect(patches.length).to.equal(1);
|
|
592
|
+
const patch = patches[0];
|
|
593
|
+
|
|
594
|
+
expect(patch).to.deep.include({
|
|
595
|
+
op: 'insertAfter',
|
|
596
|
+
selector: '#selector',
|
|
597
|
+
value: 'Content with tag',
|
|
598
|
+
valueFormat: 'hast',
|
|
599
|
+
tag: 'div',
|
|
600
|
+
opportunityId: 'opp-tag',
|
|
601
|
+
suggestionId: 'sugg-with-tag',
|
|
602
|
+
prerenderRequired: true,
|
|
603
|
+
});
|
|
604
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should not include tag when not provided', () => {
|
|
608
|
+
const suggestion = {
|
|
609
|
+
getId: () => 'sugg-no-tag',
|
|
610
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
611
|
+
getData: () => ({
|
|
612
|
+
transformRules: {
|
|
613
|
+
action: 'insertAfter',
|
|
614
|
+
selector: '#selector',
|
|
615
|
+
},
|
|
616
|
+
patchValue: 'Content without tag',
|
|
617
|
+
url: 'https://example.com/page',
|
|
618
|
+
}),
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-no-tag');
|
|
622
|
+
|
|
623
|
+
expect(patches.length).to.equal(1);
|
|
624
|
+
const patch = patches[0];
|
|
625
|
+
|
|
626
|
+
expect(patch.tag).to.be.undefined;
|
|
627
|
+
expect(patch.valueFormat).to.equal('text');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should not include UI-only fields in patch', () => {
|
|
631
|
+
const suggestion = {
|
|
632
|
+
getId: () => 'sugg-ui',
|
|
633
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
634
|
+
getData: () => ({
|
|
635
|
+
transformRules: {
|
|
636
|
+
action: 'insertAfter',
|
|
637
|
+
selector: '#selector',
|
|
638
|
+
},
|
|
639
|
+
patchValue: 'Text content',
|
|
640
|
+
url: 'https://example.com/page',
|
|
641
|
+
contentBefore: 'Original content',
|
|
642
|
+
expectedContentAfter: 'Expected result',
|
|
643
|
+
rationale: 'This improves SEO',
|
|
644
|
+
aggregationKey: 'some-key',
|
|
645
|
+
}),
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-ui');
|
|
649
|
+
|
|
650
|
+
expect(patches.length).to.equal(1);
|
|
651
|
+
const patch = patches[0];
|
|
652
|
+
|
|
653
|
+
// Should not include UI-only fields
|
|
654
|
+
expect(patch.contentBefore).to.be.undefined;
|
|
655
|
+
expect(patch.expectedContentAfter).to.be.undefined;
|
|
656
|
+
expect(patch.rationale).to.be.undefined;
|
|
657
|
+
expect(patch.aggregationKey).to.be.undefined;
|
|
658
|
+
|
|
659
|
+
// Should include only operational fields
|
|
660
|
+
expect(patch.op).to.equal('insertAfter');
|
|
661
|
+
expect(patch.selector).to.equal('#selector');
|
|
662
|
+
expect(patch.value).to.equal('Text content');
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
});
|
|
@@ -46,7 +46,7 @@ describe('ReadabilityMapper', () => {
|
|
|
46
46
|
const suggestion = {
|
|
47
47
|
getData: () => ({
|
|
48
48
|
textPreview: 'Lorem ipsum...',
|
|
49
|
-
|
|
49
|
+
url: 'https://www.website.com',
|
|
50
50
|
transformRules: {
|
|
51
51
|
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
52
52
|
op: 'replace',
|
|
@@ -66,7 +66,7 @@ describe('ReadabilityMapper', () => {
|
|
|
66
66
|
const suggestion = {
|
|
67
67
|
getData: () => ({
|
|
68
68
|
textPreview: 'Lorem ipsum...',
|
|
69
|
-
|
|
69
|
+
url: 'https://www.website.com',
|
|
70
70
|
transformRules: {
|
|
71
71
|
value: 'Improved readability text...',
|
|
72
72
|
op: 'replace',
|
|
@@ -84,7 +84,7 @@ describe('ReadabilityMapper', () => {
|
|
|
84
84
|
const suggestion = {
|
|
85
85
|
getData: () => ({
|
|
86
86
|
textPreview: 'Lorem ipsum...',
|
|
87
|
-
|
|
87
|
+
url: 'https://www.website.com',
|
|
88
88
|
}),
|
|
89
89
|
};
|
|
90
90
|
|
|
@@ -113,7 +113,7 @@ describe('ReadabilityMapper', () => {
|
|
|
113
113
|
const suggestion = {
|
|
114
114
|
getData: () => ({
|
|
115
115
|
textPreview: 'Text...',
|
|
116
|
-
|
|
116
|
+
url: 'https://www.example.com',
|
|
117
117
|
transformRules: {
|
|
118
118
|
value: 'New text',
|
|
119
119
|
op: 'replace',
|
|
@@ -133,7 +133,7 @@ describe('ReadabilityMapper', () => {
|
|
|
133
133
|
const suggestion = {
|
|
134
134
|
getData: () => ({
|
|
135
135
|
textPreview: 'Text...',
|
|
136
|
-
|
|
136
|
+
url: 'https://www.example.com',
|
|
137
137
|
transformRules: {
|
|
138
138
|
value: 'New text',
|
|
139
139
|
selector: '#content',
|
|
@@ -153,7 +153,7 @@ describe('ReadabilityMapper', () => {
|
|
|
153
153
|
const suggestion = {
|
|
154
154
|
getData: () => ({
|
|
155
155
|
textPreview: 'Text...',
|
|
156
|
-
|
|
156
|
+
url: 'https://www.example.com',
|
|
157
157
|
transformRules: {
|
|
158
158
|
value: 'New text',
|
|
159
159
|
op: 'insertAfter',
|
|
@@ -174,7 +174,7 @@ describe('ReadabilityMapper', () => {
|
|
|
174
174
|
const suggestion = {
|
|
175
175
|
getData: () => ({
|
|
176
176
|
textPreview: 'Text...',
|
|
177
|
-
|
|
177
|
+
url: 'https://www.example.com',
|
|
178
178
|
transformRules: {
|
|
179
179
|
op: 'replace',
|
|
180
180
|
selector: '#content',
|
|
@@ -194,7 +194,7 @@ describe('ReadabilityMapper', () => {
|
|
|
194
194
|
const suggestion = {
|
|
195
195
|
getData: () => ({
|
|
196
196
|
textPreview: 'Text...',
|
|
197
|
-
|
|
197
|
+
url: 'https://www.example.com',
|
|
198
198
|
transformRules: {
|
|
199
199
|
value: 'New text',
|
|
200
200
|
op: 'replace',
|
|
@@ -211,11 +211,11 @@ describe('ReadabilityMapper', () => {
|
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
it('should return ineligible when
|
|
214
|
+
it('should return ineligible when url is invalid', () => {
|
|
215
215
|
const suggestion = {
|
|
216
216
|
getData: () => ({
|
|
217
217
|
textPreview: 'Text...',
|
|
218
|
-
|
|
218
|
+
url: 'not-a-valid-url',
|
|
219
219
|
transformRules: {
|
|
220
220
|
value: 'New text',
|
|
221
221
|
op: 'replace',
|
|
@@ -228,11 +228,11 @@ describe('ReadabilityMapper', () => {
|
|
|
228
228
|
|
|
229
229
|
expect(result).to.deep.equal({
|
|
230
230
|
eligible: false,
|
|
231
|
-
reason: '
|
|
231
|
+
reason: 'url not-a-valid-url is not a valid URL',
|
|
232
232
|
});
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
it('should return ineligible when
|
|
235
|
+
it('should return ineligible when url is missing', () => {
|
|
236
236
|
const suggestion = {
|
|
237
237
|
getData: () => ({
|
|
238
238
|
textPreview: 'Text...',
|
|
@@ -248,7 +248,7 @@ describe('ReadabilityMapper', () => {
|
|
|
248
248
|
|
|
249
249
|
expect(result).to.deep.equal({
|
|
250
250
|
eligible: false,
|
|
251
|
-
reason: '
|
|
251
|
+
reason: 'url undefined is not a valid URL',
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
254
|
});
|
|
@@ -260,7 +260,7 @@ describe('ReadabilityMapper', () => {
|
|
|
260
260
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
261
261
|
getData: () => ({
|
|
262
262
|
textPreview: 'Lorem ipsum...',
|
|
263
|
-
|
|
263
|
+
url: 'https://www.website.com',
|
|
264
264
|
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
265
265
|
transformRules: {
|
|
266
266
|
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
@@ -296,7 +296,7 @@ describe('ReadabilityMapper', () => {
|
|
|
296
296
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
297
297
|
getData: () => ({
|
|
298
298
|
textPreview: 'Original text...',
|
|
299
|
-
|
|
299
|
+
url: 'https://www.example.com',
|
|
300
300
|
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
301
301
|
transformRules: {
|
|
302
302
|
value: 'Improved readability text',
|
|
@@ -319,7 +319,7 @@ describe('ReadabilityMapper', () => {
|
|
|
319
319
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
320
320
|
getData: () => ({
|
|
321
321
|
textPreview: 'Text...',
|
|
322
|
-
|
|
322
|
+
url: 'https://www.example.com',
|
|
323
323
|
transformRules: {
|
|
324
324
|
value: 'Better text',
|
|
325
325
|
op: 'replace',
|
|
@@ -341,7 +341,7 @@ describe('ReadabilityMapper', () => {
|
|
|
341
341
|
getId: () => 'sugg-999',
|
|
342
342
|
getData: () => ({
|
|
343
343
|
textPreview: 'Text...',
|
|
344
|
-
|
|
344
|
+
url: 'https://www.example.com',
|
|
345
345
|
// Missing transformRules
|
|
346
346
|
}),
|
|
347
347
|
};
|
|
@@ -364,7 +364,7 @@ describe('ReadabilityMapper', () => {
|
|
|
364
364
|
getId: () => 'sugg-warn',
|
|
365
365
|
getData: () => ({
|
|
366
366
|
textPreview: 'Text...',
|
|
367
|
-
|
|
367
|
+
url: 'https://www.example.com',
|
|
368
368
|
transformRules: {
|
|
369
369
|
op: 'replace',
|
|
370
370
|
// Missing selector and value
|
|
@@ -386,7 +386,7 @@ describe('ReadabilityMapper', () => {
|
|
|
386
386
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
387
387
|
getData: () => ({
|
|
388
388
|
textPreview: 'Original text 1',
|
|
389
|
-
|
|
389
|
+
url: 'https://www.example.com/page1',
|
|
390
390
|
transformRules: {
|
|
391
391
|
value: 'First improved text',
|
|
392
392
|
op: 'replace',
|
|
@@ -399,7 +399,7 @@ describe('ReadabilityMapper', () => {
|
|
|
399
399
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
400
400
|
getData: () => ({
|
|
401
401
|
textPreview: 'Original text 2',
|
|
402
|
-
|
|
402
|
+
url: 'https://www.example.com/page2',
|
|
403
403
|
transformRules: {
|
|
404
404
|
value: 'Second improved text',
|
|
405
405
|
op: 'replace',
|