@adobe/spacecat-shared-tokowaka-client 1.5.5 → 1.5.7
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 +15 -0
- package/package.json +1 -1
- package/src/index.js +8 -3
- package/src/mappers/base-mapper.js +1 -4
- package/src/mappers/faq-mapper.js +1 -4
- package/src/mappers/headings-mapper.js +4 -0
- package/test/index.test.js +165 -1
- package/test/mappers/base-mapper.test.js +0 -74
- package/test/mappers/headings-mapper.test.js +241 -7
- package/test/mappers/readability-mapper.test.js +2 -4
- package/test/mappers/toc-mapper.test.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.6...@adobe/spacecat-shared-tokowaka-client-v1.5.7) (2026-01-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* headings mapper for invalid headings ([#1281](https://github.com/adobe/spacecat-shared/issues/1281)) ([01734df](https://github.com/adobe/spacecat-shared/commit/01734df0de2f9d43bb6c2ee0636449c26d336ca7))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.5...@adobe/spacecat-shared-tokowaka-client-v1.5.6) (2026-01-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* edge apis, ignore scrapedAt prop for lastUpdated calculation ([#1280](https://github.com/adobe/spacecat-shared/issues/1280)) ([05e4bde](https://github.com/adobe/spacecat-shared/commit/05e4bde07fc865e8d0aee71639f799798e68594d))
|
|
14
|
+
* update prerender config ([#1279](https://github.com/adobe/spacecat-shared/issues/1279)) ([4502ba4](https://github.com/adobe/spacecat-shared/commit/4502ba43f6a26e5d774ce0e2e7dfc6e886ab7402))
|
|
15
|
+
|
|
1
16
|
# [@adobe/spacecat-shared-tokowaka-client-v1.5.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.4...@adobe/spacecat-shared-tokowaka-client-v1.5.5) (2026-01-21)
|
|
2
17
|
|
|
3
18
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -355,20 +355,25 @@ class TokowakaClient {
|
|
|
355
355
|
?? existingMetaconfig.forceFail
|
|
356
356
|
?? false;
|
|
357
357
|
|
|
358
|
+
const hasPrerender = isNonEmptyObject(options.prerender)
|
|
359
|
+
|| isNonEmptyObject(existingMetaconfig.prerender);
|
|
360
|
+
const prerender = options.prerender
|
|
361
|
+
?? existingMetaconfig.prerender;
|
|
362
|
+
|
|
358
363
|
const metaconfig = {
|
|
359
|
-
|
|
360
|
-
apiKeys: existingMetaconfig.apiKeys,
|
|
364
|
+
...existingMetaconfig,
|
|
361
365
|
tokowakaEnabled: options.tokowakaEnabled ?? existingMetaconfig.tokowakaEnabled ?? true,
|
|
362
366
|
enhancements: options.enhancements ?? existingMetaconfig.enhancements ?? true,
|
|
363
367
|
patches: isNonEmptyObject(options.patches)
|
|
364
368
|
? options.patches
|
|
365
369
|
: (existingMetaconfig.patches ?? {}),
|
|
366
370
|
...(hasForceFail && { forceFail }),
|
|
371
|
+
...(hasPrerender && { prerender }),
|
|
367
372
|
};
|
|
368
373
|
|
|
369
374
|
const s3Path = await this.uploadMetaconfig(url, metaconfig);
|
|
370
375
|
|
|
371
|
-
this.log.info(`
|
|
376
|
+
this.log.info(`Updated Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
|
|
372
377
|
|
|
373
378
|
return metaconfig;
|
|
374
379
|
}
|
|
@@ -88,10 +88,7 @@ export default class BaseOpportunityMapper {
|
|
|
88
88
|
* @returns {Object} - Base patch object
|
|
89
89
|
*/
|
|
90
90
|
createBasePatch(suggestion, opportunityId) {
|
|
91
|
-
const
|
|
92
|
-
const updatedAt = data?.scrapedAt
|
|
93
|
-
|| data?.transformRules?.scrapedAt
|
|
94
|
-
|| suggestion.getUpdatedAt();
|
|
91
|
+
const updatedAt = suggestion.getUpdatedAt();
|
|
95
92
|
|
|
96
93
|
// Parse timestamp, fallback to Date.now() if invalid
|
|
97
94
|
let lastUpdated = Date.now();
|
|
@@ -111,10 +111,7 @@ export default class FaqMapper extends BaseOpportunityMapper {
|
|
|
111
111
|
// Calculate the most recent lastUpdated from all eligible suggestions
|
|
112
112
|
// The heading patch should have the same timestamp as the newest FAQ
|
|
113
113
|
const maxLastUpdated = Math.max(...eligibleSuggestions.map((suggestion) => {
|
|
114
|
-
const
|
|
115
|
-
const updatedAt = data?.scrapedAt
|
|
116
|
-
|| data?.transformRules?.scrapedAt
|
|
117
|
-
|| suggestion.getUpdatedAt();
|
|
114
|
+
const updatedAt = suggestion.getUpdatedAt();
|
|
118
115
|
|
|
119
116
|
if (updatedAt) {
|
|
120
117
|
const parsed = new Date(updatedAt).getTime();
|
|
@@ -67,6 +67,10 @@ export default class HeadingsMapper extends BaseOpportunityMapper {
|
|
|
67
67
|
patch.tag = transformRules.tag;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
if (checkType === 'heading-order-invalid') {
|
|
71
|
+
patch.value = transformRules.value;
|
|
72
|
+
patch.valueFormat = transformRules.valueFormat;
|
|
73
|
+
}
|
|
70
74
|
patches.push(patch);
|
|
71
75
|
});
|
|
72
76
|
|
package/test/index.test.js
CHANGED
|
@@ -590,7 +590,7 @@ describe('TokowakaClient', () => {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
it('should update metaconfig with default options', async () => {
|
|
593
|
-
const siteId = 'site-
|
|
593
|
+
const siteId = 'site-456';
|
|
594
594
|
const url = 'https://www.example.com/page1';
|
|
595
595
|
|
|
596
596
|
const result = await client.updateMetaconfig(url, siteId);
|
|
@@ -1144,6 +1144,170 @@ describe('TokowakaClient', () => {
|
|
|
1144
1144
|
|
|
1145
1145
|
expect(result).to.have.property('forceFail', false);
|
|
1146
1146
|
});
|
|
1147
|
+
|
|
1148
|
+
it('should include prerender when provided in options', async () => {
|
|
1149
|
+
const siteId = 'site-789';
|
|
1150
|
+
const url = 'https://example.com';
|
|
1151
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1152
|
+
|
|
1153
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1154
|
+
|
|
1155
|
+
expect(result).to.have.property('prerender');
|
|
1156
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('should preserve existingMetaconfig prerender when options.prerender is undefined', async () => {
|
|
1160
|
+
const existingPrerenderConfig = { allowList: ['/*', '/products/*'] };
|
|
1161
|
+
const configWithPrerender = {
|
|
1162
|
+
siteId: 'site-456',
|
|
1163
|
+
apiKeys: ['existing-api-key-123'],
|
|
1164
|
+
tokowakaEnabled: false,
|
|
1165
|
+
enhancements: false,
|
|
1166
|
+
patches: {},
|
|
1167
|
+
prerender: existingPrerenderConfig,
|
|
1168
|
+
};
|
|
1169
|
+
s3Client.send.onFirstCall().resolves({
|
|
1170
|
+
Body: {
|
|
1171
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const siteId = 'site-789';
|
|
1176
|
+
const url = 'https://example.com';
|
|
1177
|
+
|
|
1178
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1179
|
+
|
|
1180
|
+
expect(result).to.have.property('prerender');
|
|
1181
|
+
expect(result.prerender).to.deep.equal(existingPrerenderConfig);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it('should use existingMetaconfig prerender when options.prerender is null', async () => {
|
|
1185
|
+
const existingPrerenderConfig = { allowList: ['/*'] };
|
|
1186
|
+
const configWithPrerender = {
|
|
1187
|
+
siteId: 'site-456',
|
|
1188
|
+
apiKeys: ['existing-api-key-123'],
|
|
1189
|
+
tokowakaEnabled: false,
|
|
1190
|
+
enhancements: false,
|
|
1191
|
+
patches: {},
|
|
1192
|
+
prerender: existingPrerenderConfig,
|
|
1193
|
+
};
|
|
1194
|
+
s3Client.send.onFirstCall().resolves({
|
|
1195
|
+
Body: {
|
|
1196
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const siteId = 'site-789';
|
|
1201
|
+
const url = 'https://example.com';
|
|
1202
|
+
|
|
1203
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: null });
|
|
1204
|
+
|
|
1205
|
+
expect(result).to.have.property('prerender');
|
|
1206
|
+
expect(result.prerender).to.deep.equal(existingPrerenderConfig);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('should override existingMetaconfig prerender when provided in options', async () => {
|
|
1210
|
+
const existingPrerenderConfig = { allowList: ['/blog/*'] };
|
|
1211
|
+
const newPrerenderConfig = { allowList: ['/*', '/products/*'] };
|
|
1212
|
+
const configWithPrerender = {
|
|
1213
|
+
siteId: 'site-456',
|
|
1214
|
+
apiKeys: ['existing-api-key-123'],
|
|
1215
|
+
tokowakaEnabled: false,
|
|
1216
|
+
enhancements: false,
|
|
1217
|
+
patches: {},
|
|
1218
|
+
prerender: existingPrerenderConfig,
|
|
1219
|
+
};
|
|
1220
|
+
s3Client.send.onFirstCall().resolves({
|
|
1221
|
+
Body: {
|
|
1222
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const siteId = 'site-789';
|
|
1227
|
+
const url = 'https://example.com';
|
|
1228
|
+
|
|
1229
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: newPrerenderConfig });
|
|
1230
|
+
|
|
1231
|
+
expect(result).to.have.property('prerender');
|
|
1232
|
+
expect(result.prerender).to.deep.equal(newPrerenderConfig);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it('should not include prerender when neither options nor existingMetaconfig have it', async () => {
|
|
1236
|
+
const siteId = 'site-789';
|
|
1237
|
+
const url = 'https://example.com';
|
|
1238
|
+
|
|
1239
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1240
|
+
|
|
1241
|
+
expect(result).to.not.have.property('prerender');
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('should not include prerender when both options and existingMetaconfig have empty prerender', async () => {
|
|
1245
|
+
const siteId = 'site-789';
|
|
1246
|
+
const url = 'https://example.com';
|
|
1247
|
+
|
|
1248
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: {} });
|
|
1249
|
+
|
|
1250
|
+
expect(result).to.not.have.property('prerender');
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('should include prerender from options when existingMetaconfig does not have it', async () => {
|
|
1254
|
+
const siteId = 'site-789';
|
|
1255
|
+
const url = 'https://example.com';
|
|
1256
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1257
|
+
|
|
1258
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1259
|
+
|
|
1260
|
+
expect(result).to.have.property('prerender');
|
|
1261
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('should handle prerender with multiple paths in allowList', async () => {
|
|
1265
|
+
const siteId = 'site-789';
|
|
1266
|
+
const url = 'https://example.com';
|
|
1267
|
+
const prerenderConfig = {
|
|
1268
|
+
allowList: ['/*', '/products/*', '/blog/*', '/about'],
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1272
|
+
|
|
1273
|
+
expect(result).to.have.property('prerender');
|
|
1274
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('should use options.prerender even when it is an empty object if existingMetaconfig has no prerender', async () => {
|
|
1278
|
+
const siteId = 'site-789';
|
|
1279
|
+
const url = 'https://example.com';
|
|
1280
|
+
|
|
1281
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: {} });
|
|
1282
|
+
|
|
1283
|
+
// Empty object is not null/undefined, so it will be used by nullish coalescing
|
|
1284
|
+
// But hasPrerender will be false, so it won't be included in final metaconfig
|
|
1285
|
+
expect(result).to.not.have.property('prerender');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('should handle case where existingMetaconfig.prerender is undefined and options.prerender is provided', async () => {
|
|
1289
|
+
const configWithoutPrerender = {
|
|
1290
|
+
siteId: 'site-456',
|
|
1291
|
+
apiKeys: ['existing-api-key-123'],
|
|
1292
|
+
tokowakaEnabled: false,
|
|
1293
|
+
enhancements: false,
|
|
1294
|
+
patches: {},
|
|
1295
|
+
};
|
|
1296
|
+
s3Client.send.onFirstCall().resolves({
|
|
1297
|
+
Body: {
|
|
1298
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithoutPrerender)),
|
|
1299
|
+
},
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
const siteId = 'site-789';
|
|
1303
|
+
const url = 'https://example.com';
|
|
1304
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1305
|
+
|
|
1306
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1307
|
+
|
|
1308
|
+
expect(result).to.have.property('prerender');
|
|
1309
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1310
|
+
});
|
|
1147
1311
|
});
|
|
1148
1312
|
|
|
1149
1313
|
describe('uploadConfig', () => {
|
|
@@ -68,7 +68,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
68
68
|
const testMapper = new TestMapper(log);
|
|
69
69
|
const suggestion = {
|
|
70
70
|
getId: () => 'test-123',
|
|
71
|
-
getData: () => ({}),
|
|
72
71
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
73
72
|
};
|
|
74
73
|
|
|
@@ -94,7 +93,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
94
93
|
const testMapper = new TestMapper(log);
|
|
95
94
|
const suggestion = {
|
|
96
95
|
getId: () => 'test-no-date',
|
|
97
|
-
getData: () => ({}),
|
|
98
96
|
getUpdatedAt: () => null, // Returns null
|
|
99
97
|
};
|
|
100
98
|
|
|
@@ -109,54 +107,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
109
107
|
expect(patch.prerenderRequired).to.be.true;
|
|
110
108
|
});
|
|
111
109
|
|
|
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
110
|
it('should handle invalid date strings by using Date.now()', () => {
|
|
161
111
|
class TestMapper extends BaseOpportunityMapper {
|
|
162
112
|
getOpportunityType() { return 'test'; }
|
|
@@ -171,7 +121,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
171
121
|
const testMapper = new TestMapper(log);
|
|
172
122
|
const suggestion = {
|
|
173
123
|
getId: () => 'test-invalid',
|
|
174
|
-
getData: () => ({}),
|
|
175
124
|
getUpdatedAt: () => 'invalid-date-string',
|
|
176
125
|
};
|
|
177
126
|
|
|
@@ -183,29 +132,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
183
132
|
expect(patch.lastUpdated).to.be.at.least(beforeTime);
|
|
184
133
|
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
185
134
|
});
|
|
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
|
-
});
|
|
209
135
|
});
|
|
210
136
|
|
|
211
137
|
describe('rollbackPatches', () => {
|
|
@@ -98,11 +98,12 @@ describe('HeadingsMapper', () => {
|
|
|
98
98
|
const suggestion = {
|
|
99
99
|
getData: () => ({
|
|
100
100
|
checkType: 'heading-order-invalid',
|
|
101
|
-
recommendedAction:
|
|
101
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
102
102
|
transformRules: {
|
|
103
103
|
action: 'replaceWith',
|
|
104
104
|
selector: '.invalid-section',
|
|
105
105
|
valueFormat: 'hast',
|
|
106
|
+
value: { type: 'element', tagName: 'h2', children: [] },
|
|
106
107
|
},
|
|
107
108
|
}),
|
|
108
109
|
};
|
|
@@ -254,11 +255,12 @@ describe('HeadingsMapper', () => {
|
|
|
254
255
|
const suggestion = {
|
|
255
256
|
getData: () => ({
|
|
256
257
|
checkType: 'heading-order-invalid',
|
|
257
|
-
recommendedAction:
|
|
258
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
258
259
|
transformRules: {
|
|
259
260
|
action: 'replace',
|
|
260
261
|
selector: '.invalid-section',
|
|
261
262
|
valueFormat: 'hast',
|
|
263
|
+
value: { type: 'element', tagName: 'h2', children: [] },
|
|
262
264
|
},
|
|
263
265
|
}),
|
|
264
266
|
};
|
|
@@ -275,10 +277,11 @@ describe('HeadingsMapper', () => {
|
|
|
275
277
|
const suggestion = {
|
|
276
278
|
getData: () => ({
|
|
277
279
|
checkType: 'heading-order-invalid',
|
|
278
|
-
recommendedAction:
|
|
280
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
279
281
|
transformRules: {
|
|
280
282
|
action: 'replaceWith',
|
|
281
283
|
selector: '.invalid-section',
|
|
284
|
+
value: { type: 'element', tagName: 'h2', children: [] },
|
|
282
285
|
},
|
|
283
286
|
}),
|
|
284
287
|
};
|
|
@@ -295,11 +298,12 @@ describe('HeadingsMapper', () => {
|
|
|
295
298
|
const suggestion = {
|
|
296
299
|
getData: () => ({
|
|
297
300
|
checkType: 'heading-order-invalid',
|
|
298
|
-
recommendedAction:
|
|
301
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
299
302
|
transformRules: {
|
|
300
303
|
action: 'replaceWith',
|
|
301
304
|
selector: '.invalid-section',
|
|
302
305
|
valueFormat: 'text',
|
|
306
|
+
value: { type: 'element', tagName: 'h2', children: [] },
|
|
303
307
|
},
|
|
304
308
|
}),
|
|
305
309
|
};
|
|
@@ -447,11 +451,12 @@ describe('HeadingsMapper', () => {
|
|
|
447
451
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
448
452
|
getData: () => ({
|
|
449
453
|
checkType: 'heading-order-invalid',
|
|
450
|
-
recommendedAction:
|
|
454
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
451
455
|
transformRules: {
|
|
452
456
|
action: 'replaceWith',
|
|
453
457
|
selector: '.content-section',
|
|
454
458
|
valueFormat: 'hast',
|
|
459
|
+
value: hastValue,
|
|
455
460
|
},
|
|
456
461
|
}),
|
|
457
462
|
};
|
|
@@ -464,6 +469,7 @@ describe('HeadingsMapper', () => {
|
|
|
464
469
|
op: 'replaceWith',
|
|
465
470
|
selector: '.content-section',
|
|
466
471
|
value: hastValue,
|
|
472
|
+
valueFormat: 'hast',
|
|
467
473
|
opportunityId: 'opp-101',
|
|
468
474
|
suggestionId: 'sugg-101',
|
|
469
475
|
prerenderRequired: true,
|
|
@@ -477,7 +483,7 @@ describe('HeadingsMapper', () => {
|
|
|
477
483
|
getId: () => 'sugg-102',
|
|
478
484
|
getData: () => ({
|
|
479
485
|
checkType: 'heading-order-invalid',
|
|
480
|
-
recommendedAction:
|
|
486
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
481
487
|
}),
|
|
482
488
|
};
|
|
483
489
|
|
|
@@ -490,11 +496,12 @@ describe('HeadingsMapper', () => {
|
|
|
490
496
|
getId: () => 'sugg-103',
|
|
491
497
|
getData: () => ({
|
|
492
498
|
checkType: 'heading-order-invalid',
|
|
493
|
-
recommendedAction:
|
|
499
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
494
500
|
transformRules: {
|
|
495
501
|
action: 'replace',
|
|
496
502
|
selector: '.content-section',
|
|
497
503
|
valueFormat: 'hast',
|
|
504
|
+
value: { type: 'element', tagName: 'h2', children: [] },
|
|
498
505
|
},
|
|
499
506
|
}),
|
|
500
507
|
};
|
|
@@ -503,6 +510,233 @@ describe('HeadingsMapper', () => {
|
|
|
503
510
|
expect(patches.length).to.equal(0);
|
|
504
511
|
});
|
|
505
512
|
|
|
513
|
+
it('should create patch for heading-order-invalid with real-world HAST structure', () => {
|
|
514
|
+
const hastValue = {
|
|
515
|
+
type: 'root',
|
|
516
|
+
children: [
|
|
517
|
+
{
|
|
518
|
+
type: 'element',
|
|
519
|
+
tagName: 'h2',
|
|
520
|
+
children: [
|
|
521
|
+
{
|
|
522
|
+
type: 'text',
|
|
523
|
+
value: 'Complete Cover Sets',
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
properties: {},
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const suggestion = {
|
|
532
|
+
getId: () => 'sugg-104',
|
|
533
|
+
getUpdatedAt: () => '2026-01-03T06:24:06.229Z',
|
|
534
|
+
getData: () => ({
|
|
535
|
+
checkType: 'heading-order-invalid',
|
|
536
|
+
recommendedAction: 'Adjust heading levels to maintain proper hierarchy.',
|
|
537
|
+
transformRules: {
|
|
538
|
+
action: 'replaceWith',
|
|
539
|
+
selector: 'h4#complete-cover-sets',
|
|
540
|
+
valueFormat: 'hast',
|
|
541
|
+
value: hastValue,
|
|
542
|
+
scrapedAt: '2026-01-03T06:24:06.229Z',
|
|
543
|
+
currValue: 'Complete Cover Sets',
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-104');
|
|
549
|
+
expect(patches.length).to.equal(1);
|
|
550
|
+
const patch = patches[0];
|
|
551
|
+
|
|
552
|
+
expect(patch).to.deep.include({
|
|
553
|
+
op: 'replaceWith',
|
|
554
|
+
selector: 'h4#complete-cover-sets',
|
|
555
|
+
value: hastValue,
|
|
556
|
+
valueFormat: 'hast',
|
|
557
|
+
opportunityId: 'opp-104',
|
|
558
|
+
suggestionId: 'sugg-104',
|
|
559
|
+
prerenderRequired: true,
|
|
560
|
+
});
|
|
561
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should create patch for heading-order-invalid with complex selector', () => {
|
|
565
|
+
const hastValue = {
|
|
566
|
+
type: 'root',
|
|
567
|
+
children: [
|
|
568
|
+
{
|
|
569
|
+
type: 'element',
|
|
570
|
+
tagName: 'h3',
|
|
571
|
+
properties: { id: 'section-title' },
|
|
572
|
+
children: [{ type: 'text', value: 'Updated Section' }],
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const suggestion = {
|
|
578
|
+
getId: () => 'sugg-105',
|
|
579
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
580
|
+
getData: () => ({
|
|
581
|
+
checkType: 'heading-order-invalid',
|
|
582
|
+
recommendedAction: 'Fix heading hierarchy from h5 to h3.',
|
|
583
|
+
transformRules: {
|
|
584
|
+
action: 'replaceWith',
|
|
585
|
+
selector: 'body > main > section:nth-child(2) > h5',
|
|
586
|
+
valueFormat: 'hast',
|
|
587
|
+
value: hastValue,
|
|
588
|
+
},
|
|
589
|
+
}),
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-105');
|
|
593
|
+
expect(patches.length).to.equal(1);
|
|
594
|
+
const patch = patches[0];
|
|
595
|
+
|
|
596
|
+
expect(patch.selector).to.equal('body > main > section:nth-child(2) > h5');
|
|
597
|
+
expect(patch.value).to.deep.equal(hastValue);
|
|
598
|
+
expect(patch.valueFormat).to.equal('hast');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should handle multiple heading-order-invalid suggestions', () => {
|
|
602
|
+
const suggestions = [
|
|
603
|
+
{
|
|
604
|
+
getId: () => 'sugg-106',
|
|
605
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
606
|
+
getData: () => ({
|
|
607
|
+
checkType: 'heading-order-invalid',
|
|
608
|
+
recommendedAction: 'Fix heading hierarchy.',
|
|
609
|
+
transformRules: {
|
|
610
|
+
action: 'replaceWith',
|
|
611
|
+
selector: 'h4.title',
|
|
612
|
+
valueFormat: 'hast',
|
|
613
|
+
value: {
|
|
614
|
+
type: 'root',
|
|
615
|
+
children: [
|
|
616
|
+
{
|
|
617
|
+
type: 'element',
|
|
618
|
+
tagName: 'h2',
|
|
619
|
+
properties: { class: 'title' },
|
|
620
|
+
children: [{ type: 'text', value: 'Title 1' }],
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
}),
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
getId: () => 'sugg-107',
|
|
629
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
630
|
+
getData: () => ({
|
|
631
|
+
checkType: 'heading-order-invalid',
|
|
632
|
+
recommendedAction: 'Fix another heading.',
|
|
633
|
+
transformRules: {
|
|
634
|
+
action: 'replaceWith',
|
|
635
|
+
selector: 'h5.subtitle',
|
|
636
|
+
valueFormat: 'hast',
|
|
637
|
+
value: {
|
|
638
|
+
type: 'root',
|
|
639
|
+
children: [
|
|
640
|
+
{
|
|
641
|
+
type: 'element',
|
|
642
|
+
tagName: 'h3',
|
|
643
|
+
properties: { class: 'subtitle' },
|
|
644
|
+
children: [{ type: 'text', value: 'Subtitle' }],
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
},
|
|
651
|
+
];
|
|
652
|
+
|
|
653
|
+
const patches = mapper.suggestionsToPatches('/path', suggestions, 'opp-106');
|
|
654
|
+
expect(patches.length).to.equal(2);
|
|
655
|
+
expect(patches[0].selector).to.equal('h4.title');
|
|
656
|
+
expect(patches[1].selector).to.equal('h5.subtitle');
|
|
657
|
+
expect(patches[0].valueFormat).to.equal('hast');
|
|
658
|
+
expect(patches[1].valueFormat).to.equal('hast');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should not use text valueFormat for heading-order-invalid', () => {
|
|
662
|
+
const hastValue = {
|
|
663
|
+
type: 'root',
|
|
664
|
+
children: [
|
|
665
|
+
{ type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'Heading' }] },
|
|
666
|
+
],
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const suggestion = {
|
|
670
|
+
getId: () => 'sugg-108',
|
|
671
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
672
|
+
getData: () => ({
|
|
673
|
+
checkType: 'heading-order-invalid',
|
|
674
|
+
recommendedAction: 'Fix heading hierarchy.',
|
|
675
|
+
transformRules: {
|
|
676
|
+
action: 'replaceWith',
|
|
677
|
+
selector: 'h4',
|
|
678
|
+
valueFormat: 'hast',
|
|
679
|
+
value: hastValue,
|
|
680
|
+
},
|
|
681
|
+
}),
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-108');
|
|
685
|
+
expect(patches.length).to.equal(1);
|
|
686
|
+
const patch = patches[0];
|
|
687
|
+
|
|
688
|
+
// Verify it uses HAST format, not text
|
|
689
|
+
expect(patch.valueFormat).to.equal('hast');
|
|
690
|
+
expect(patch.value).to.be.an('object');
|
|
691
|
+
expect(patch.value).to.deep.equal(hastValue);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should include currValue when currentValue is not null for heading-empty', () => {
|
|
695
|
+
const suggestion = {
|
|
696
|
+
getId: () => 'sugg-109',
|
|
697
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
698
|
+
getData: () => ({
|
|
699
|
+
checkType: 'heading-empty',
|
|
700
|
+
recommendedAction: 'New Heading Text',
|
|
701
|
+
currentValue: 'Old Heading',
|
|
702
|
+
transformRules: {
|
|
703
|
+
action: 'replace',
|
|
704
|
+
selector: 'h2.empty',
|
|
705
|
+
},
|
|
706
|
+
}),
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-109');
|
|
710
|
+
expect(patches.length).to.equal(1);
|
|
711
|
+
const patch = patches[0];
|
|
712
|
+
|
|
713
|
+
expect(patch.currValue).to.equal('Old Heading');
|
|
714
|
+
expect(patch.value).to.equal('New Heading Text');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should not include currValue when currentValue is null', () => {
|
|
718
|
+
const suggestion = {
|
|
719
|
+
getId: () => 'sugg-110',
|
|
720
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
721
|
+
getData: () => ({
|
|
722
|
+
checkType: 'heading-empty',
|
|
723
|
+
recommendedAction: 'New Heading Text',
|
|
724
|
+
currentValue: null,
|
|
725
|
+
transformRules: {
|
|
726
|
+
action: 'replace',
|
|
727
|
+
selector: 'h2.empty',
|
|
728
|
+
},
|
|
729
|
+
}),
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-110');
|
|
733
|
+
expect(patches.length).to.equal(1);
|
|
734
|
+
const patch = patches[0];
|
|
735
|
+
|
|
736
|
+
expect(patch.currValue).to.be.undefined;
|
|
737
|
+
expect(patch.value).to.equal('New Heading Text');
|
|
738
|
+
});
|
|
739
|
+
|
|
506
740
|
it('should return empty array for heading-missing-h1 without transformRules', () => {
|
|
507
741
|
const suggestion = {
|
|
508
742
|
getId: () => 'sugg-999',
|
|
@@ -261,7 +261,6 @@ describe('ReadabilityMapper', () => {
|
|
|
261
261
|
getData: () => ({
|
|
262
262
|
textPreview: 'Lorem ipsum...',
|
|
263
263
|
url: 'https://www.website.com',
|
|
264
|
-
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
265
264
|
transformRules: {
|
|
266
265
|
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
267
266
|
op: 'replace',
|
|
@@ -290,14 +289,13 @@ describe('ReadabilityMapper', () => {
|
|
|
290
289
|
expect(patch.lastUpdated).to.be.a('number');
|
|
291
290
|
});
|
|
292
291
|
|
|
293
|
-
it('should create patch with
|
|
292
|
+
it('should create patch with updatedAt timestamp', () => {
|
|
294
293
|
const suggestion = {
|
|
295
294
|
getId: () => 'sugg-456',
|
|
296
|
-
getUpdatedAt: () => '2025-
|
|
295
|
+
getUpdatedAt: () => '2025-09-20T06:21:12.584Z',
|
|
297
296
|
getData: () => ({
|
|
298
297
|
textPreview: 'Original text...',
|
|
299
298
|
url: 'https://www.example.com',
|
|
300
|
-
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
301
299
|
transformRules: {
|
|
302
300
|
value: 'Improved readability text',
|
|
303
301
|
op: 'replace',
|