@adobe/spacecat-shared-tokowaka-client 1.3.0 → 1.3.2
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/index.d.ts +20 -1
- package/src/index.js +59 -5
- package/src/mappers/base-mapper.js +12 -0
- package/src/mappers/headings-mapper.js +16 -1
- package/src/mappers/mapper-registry.js +4 -0
- package/src/mappers/prerender-mapper.js +78 -0
- package/src/mappers/toc-mapper.js +116 -0
- package/src/utils/custom-html-utils.js +7 -7
- package/test/index.test.js +243 -0
- package/test/mappers/generic-mapper.test.js +6 -0
- package/test/mappers/headings-mapper.test.js +154 -3
- package/test/mappers/prerender-mapper.test.js +216 -0
- package/test/mappers/toc-mapper.test.js +616 -0
- package/test/utils/html-utils.test.js +5 -5
package/test/index.test.js
CHANGED
|
@@ -1040,6 +1040,59 @@ describe('TokowakaClient', () => {
|
|
|
1040
1040
|
// Both suggestions are in result but sugg-1 skipped deployment due to no patches
|
|
1041
1041
|
expect(result.succeededSuggestions).to.have.length(2);
|
|
1042
1042
|
expect(result.s3Paths).to.have.length(1); // Only one URL actually deployed
|
|
1043
|
+
expect(log.warn).to.have.been.calledWith('No config generated for URL: https://example.com/page1');
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it('should skip URL when config has no patches after generation', async () => {
|
|
1047
|
+
// Stub generateConfig to return a config with no patches (defensive check)
|
|
1048
|
+
const originalGenerateConfig = client.generateConfig.bind(client);
|
|
1049
|
+
sinon.stub(client, 'generateConfig').callsFake((url, ...args) => {
|
|
1050
|
+
const config = originalGenerateConfig(url, ...args);
|
|
1051
|
+
if (config && url === 'https://example.com/page1') {
|
|
1052
|
+
// Return config but with empty patches array (simulating edge case)
|
|
1053
|
+
return { ...config, patches: [] };
|
|
1054
|
+
}
|
|
1055
|
+
return config;
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
mockSuggestions = [
|
|
1059
|
+
{
|
|
1060
|
+
getId: () => 'sugg-1',
|
|
1061
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1062
|
+
getData: () => ({
|
|
1063
|
+
url: 'https://example.com/page1',
|
|
1064
|
+
recommendedAction: 'New Heading',
|
|
1065
|
+
checkType: 'heading-empty',
|
|
1066
|
+
transformRules: {
|
|
1067
|
+
action: 'replace',
|
|
1068
|
+
selector: 'h1',
|
|
1069
|
+
},
|
|
1070
|
+
}),
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
getId: () => 'sugg-2',
|
|
1074
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1075
|
+
getData: () => ({
|
|
1076
|
+
url: 'https://example.com/page2',
|
|
1077
|
+
recommendedAction: 'New Subtitle',
|
|
1078
|
+
checkType: 'heading-empty',
|
|
1079
|
+
transformRules: {
|
|
1080
|
+
action: 'replace',
|
|
1081
|
+
selector: 'h2',
|
|
1082
|
+
},
|
|
1083
|
+
}),
|
|
1084
|
+
},
|
|
1085
|
+
];
|
|
1086
|
+
|
|
1087
|
+
const result = await client.deploySuggestions(
|
|
1088
|
+
mockSite,
|
|
1089
|
+
mockOpportunity,
|
|
1090
|
+
mockSuggestions,
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1094
|
+
expect(result.s3Paths).to.have.length(1); // Only page2 deployed
|
|
1095
|
+
expect(log.warn).to.have.been.calledWith('No eligible suggestions to deploy for URL: https://example.com/page1');
|
|
1043
1096
|
});
|
|
1044
1097
|
|
|
1045
1098
|
it('should return early when no eligible suggestions', async () => {
|
|
@@ -1066,6 +1119,50 @@ describe('TokowakaClient', () => {
|
|
|
1066
1119
|
expect(s3Client.send).to.not.have.been.called;
|
|
1067
1120
|
});
|
|
1068
1121
|
|
|
1122
|
+
it('should deploy prerender-only suggestions with no patches', async () => {
|
|
1123
|
+
// Create prerender opportunity
|
|
1124
|
+
const prerenderOpportunity = {
|
|
1125
|
+
getId: () => 'opp-prerender-123',
|
|
1126
|
+
getType: () => 'prerender',
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// Create prerender suggestions with no transform rules (prerender-only)
|
|
1130
|
+
const prerenderSuggestions = [
|
|
1131
|
+
{
|
|
1132
|
+
getId: () => 'prerender-sugg-1',
|
|
1133
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1134
|
+
getData: () => ({
|
|
1135
|
+
url: 'https://example.com/page1',
|
|
1136
|
+
// No transform rules - prerender only
|
|
1137
|
+
}),
|
|
1138
|
+
},
|
|
1139
|
+
];
|
|
1140
|
+
|
|
1141
|
+
const result = await client.deploySuggestions(
|
|
1142
|
+
mockSite,
|
|
1143
|
+
prerenderOpportunity,
|
|
1144
|
+
prerenderSuggestions,
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1148
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1149
|
+
expect(result.s3Paths).to.have.length(1);
|
|
1150
|
+
expect(result.cdnInvalidations).to.have.length(1);
|
|
1151
|
+
|
|
1152
|
+
// Verify uploaded config has no patches but prerender is enabled
|
|
1153
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1154
|
+
expect(uploadedConfig.patches).to.have.length(0);
|
|
1155
|
+
expect(uploadedConfig.prerender).to.equal(true);
|
|
1156
|
+
expect(uploadedConfig.url).to.equal('https://example.com/page1');
|
|
1157
|
+
|
|
1158
|
+
// Verify CDN was invalidated
|
|
1159
|
+
expect(client.invalidateCdnCache).to.have.been.calledOnce;
|
|
1160
|
+
expect(client.invalidateCdnCache).to.have.been.calledWith(
|
|
1161
|
+
'https://example.com/page1',
|
|
1162
|
+
'cloudfront',
|
|
1163
|
+
);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1069
1166
|
it('should throw error for unsupported opportunity type', async () => {
|
|
1070
1167
|
mockOpportunity.getType = () => 'unsupported-type';
|
|
1071
1168
|
|
|
@@ -1222,6 +1319,70 @@ describe('TokowakaClient', () => {
|
|
|
1222
1319
|
expect(uploadedConfig.patches[0].suggestionId).to.equal('sugg-3');
|
|
1223
1320
|
});
|
|
1224
1321
|
|
|
1322
|
+
it('should rollback prerender suggestions by disabling prerender flag', async () => {
|
|
1323
|
+
// Create prerender opportunity
|
|
1324
|
+
const prerenderOpportunity = {
|
|
1325
|
+
getId: () => 'opp-prerender-123',
|
|
1326
|
+
getType: () => 'prerender',
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// Create prerender suggestions
|
|
1330
|
+
const prerenderSuggestions = [
|
|
1331
|
+
{
|
|
1332
|
+
getId: () => 'prerender-sugg-1',
|
|
1333
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1334
|
+
getData: () => ({
|
|
1335
|
+
url: 'https://example.com/page1',
|
|
1336
|
+
}),
|
|
1337
|
+
},
|
|
1338
|
+
];
|
|
1339
|
+
|
|
1340
|
+
const existingConfig = {
|
|
1341
|
+
url: 'https://example.com/page1',
|
|
1342
|
+
version: '1.0',
|
|
1343
|
+
forceFail: false,
|
|
1344
|
+
prerender: true,
|
|
1345
|
+
patches: [
|
|
1346
|
+
{
|
|
1347
|
+
op: 'replace',
|
|
1348
|
+
selector: 'h1',
|
|
1349
|
+
value: 'Heading 1',
|
|
1350
|
+
opportunityId: 'opp-other-123',
|
|
1351
|
+
suggestionId: 'other-sugg-1',
|
|
1352
|
+
prerenderRequired: false,
|
|
1353
|
+
lastUpdated: 1234567890,
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1359
|
+
|
|
1360
|
+
const result = await client.rollbackSuggestions(
|
|
1361
|
+
mockSite,
|
|
1362
|
+
prerenderOpportunity,
|
|
1363
|
+
prerenderSuggestions,
|
|
1364
|
+
);
|
|
1365
|
+
|
|
1366
|
+
expect(result.s3Paths).to.have.length(1);
|
|
1367
|
+
expect(result.s3Paths[0]).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
1368
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1369
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1370
|
+
expect(result.removedPatchesCount).to.equal(1);
|
|
1371
|
+
|
|
1372
|
+
// Verify uploaded config has prerender disabled but patches intact
|
|
1373
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1374
|
+
expect(uploadedConfig.prerender).to.equal(false);
|
|
1375
|
+
expect(uploadedConfig.patches).to.have.length(1);
|
|
1376
|
+
expect(uploadedConfig.patches[0].suggestionId).to.equal('other-sugg-1');
|
|
1377
|
+
|
|
1378
|
+
// Verify CDN was invalidated
|
|
1379
|
+
expect(client.invalidateCdnCache).to.have.been.calledOnce;
|
|
1380
|
+
expect(client.invalidateCdnCache).to.have.been.calledWith(
|
|
1381
|
+
'https://example.com/page1',
|
|
1382
|
+
'cloudfront',
|
|
1383
|
+
);
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1225
1386
|
it('should handle no existing config gracefully', async () => {
|
|
1226
1387
|
sinon.stub(client, 'fetchConfig').resolves(null);
|
|
1227
1388
|
|
|
@@ -1262,6 +1423,30 @@ describe('TokowakaClient', () => {
|
|
|
1262
1423
|
expect(s3Client.send).to.not.have.been.called;
|
|
1263
1424
|
});
|
|
1264
1425
|
|
|
1426
|
+
it('should handle missing patches property in config', async () => {
|
|
1427
|
+
const existingConfig = {
|
|
1428
|
+
url: 'https://example.com/page1',
|
|
1429
|
+
version: '1.0',
|
|
1430
|
+
forceFail: false,
|
|
1431
|
+
prerender: true,
|
|
1432
|
+
// patches property is missing
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1436
|
+
|
|
1437
|
+
const result = await client.rollbackSuggestions(
|
|
1438
|
+
mockSite,
|
|
1439
|
+
mockOpportunity,
|
|
1440
|
+
mockSuggestions,
|
|
1441
|
+
);
|
|
1442
|
+
|
|
1443
|
+
// Code marks eligible suggestions as succeeded even if patches property missing
|
|
1444
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1445
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1446
|
+
expect(result.s3Paths).to.have.length(0);
|
|
1447
|
+
expect(s3Client.send).to.not.have.been.called;
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1265
1450
|
it('should handle suggestions not found in config', async () => {
|
|
1266
1451
|
const existingConfig = {
|
|
1267
1452
|
url: 'https://example.com/page1',
|
|
@@ -1589,6 +1774,64 @@ describe('TokowakaClient', () => {
|
|
|
1589
1774
|
expect(s3Client.send).to.have.been.calledOnce;
|
|
1590
1775
|
});
|
|
1591
1776
|
|
|
1777
|
+
it('should preview prerender-only suggestions with no patches', async () => {
|
|
1778
|
+
// Update fetchConfig to return existing config with deployed patches
|
|
1779
|
+
client.fetchConfig.resolves({
|
|
1780
|
+
url: 'https://example.com/page1',
|
|
1781
|
+
version: '1.0',
|
|
1782
|
+
forceFail: false,
|
|
1783
|
+
prerender: false,
|
|
1784
|
+
patches: [
|
|
1785
|
+
{
|
|
1786
|
+
op: 'replace',
|
|
1787
|
+
selector: 'h1',
|
|
1788
|
+
value: 'Existing Heading',
|
|
1789
|
+
opportunityId: 'opp-other-123',
|
|
1790
|
+
suggestionId: 'sugg-other',
|
|
1791
|
+
prerenderRequired: false,
|
|
1792
|
+
lastUpdated: 1234567890,
|
|
1793
|
+
},
|
|
1794
|
+
],
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
// Create prerender opportunity
|
|
1798
|
+
const prerenderOpportunity = {
|
|
1799
|
+
getId: () => 'opp-prerender-123',
|
|
1800
|
+
getType: () => 'prerender',
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
// Create prerender suggestions with no transform rules (prerender-only)
|
|
1804
|
+
const prerenderSuggestions = [
|
|
1805
|
+
{
|
|
1806
|
+
getId: () => 'prerender-sugg-1',
|
|
1807
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1808
|
+
getData: () => ({
|
|
1809
|
+
url: 'https://example.com/page1',
|
|
1810
|
+
// No transform rules - prerender only
|
|
1811
|
+
}),
|
|
1812
|
+
},
|
|
1813
|
+
];
|
|
1814
|
+
|
|
1815
|
+
const result = await client.previewSuggestions(
|
|
1816
|
+
mockSite,
|
|
1817
|
+
prerenderOpportunity,
|
|
1818
|
+
prerenderSuggestions,
|
|
1819
|
+
{ warmupDelayMs: 0 },
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
expect(result).to.have.property('s3Path');
|
|
1823
|
+
expect(result.config).to.not.be.null;
|
|
1824
|
+
expect(result.config.patches).to.have.length(1); // Merged with existing deployed patch
|
|
1825
|
+
expect(result.config.prerender).to.equal(true); // Prerender enabled
|
|
1826
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1827
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1828
|
+
expect(result).to.have.property('html');
|
|
1829
|
+
|
|
1830
|
+
// Verify fetch was called for HTML fetching
|
|
1831
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
1832
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1592
1835
|
it('should throw error if TOKOWAKA_EDGE_URL is not configured', async () => {
|
|
1593
1836
|
delete client.env.TOKOWAKA_EDGE_URL;
|
|
1594
1837
|
|
|
@@ -41,6 +41,12 @@ describe('GenericMapper', () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
describe('allowConfigsWithoutPatch', () => {
|
|
45
|
+
it('should return false for generic mapper', () => {
|
|
46
|
+
expect(mapper.allowConfigsWithoutPatch()).to.be.false;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
44
50
|
describe('canDeploy', () => {
|
|
45
51
|
it('should return eligible for valid suggestion with all required fields', () => {
|
|
46
52
|
const suggestion = {
|
|
@@ -94,6 +94,24 @@ describe('HeadingsMapper', () => {
|
|
|
94
94
|
expect(result).to.deep.equal({ eligible: true });
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it('should return eligible for heading-order-invalid checkType', () => {
|
|
98
|
+
const suggestion = {
|
|
99
|
+
getData: () => ({
|
|
100
|
+
checkType: 'heading-order-invalid',
|
|
101
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
102
|
+
transformRules: {
|
|
103
|
+
action: 'replaceWith',
|
|
104
|
+
selector: '.invalid-section',
|
|
105
|
+
valueFormat: 'hast',
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = mapper.canDeploy(suggestion);
|
|
111
|
+
|
|
112
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
97
115
|
it('should return ineligible for unknown checkType', () => {
|
|
98
116
|
const suggestion = {
|
|
99
117
|
getData: () => ({ checkType: 'unknown-type' }),
|
|
@@ -103,7 +121,7 @@ describe('HeadingsMapper', () => {
|
|
|
103
121
|
|
|
104
122
|
expect(result).to.deep.equal({
|
|
105
123
|
eligible: false,
|
|
106
|
-
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length can be deployed. This suggestion has checkType: unknown-type',
|
|
124
|
+
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length, heading-order-invalid can be deployed. This suggestion has checkType: unknown-type',
|
|
107
125
|
});
|
|
108
126
|
});
|
|
109
127
|
|
|
@@ -116,7 +134,7 @@ describe('HeadingsMapper', () => {
|
|
|
116
134
|
|
|
117
135
|
expect(result).to.deep.equal({
|
|
118
136
|
eligible: false,
|
|
119
|
-
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length can be deployed. This suggestion has checkType: undefined',
|
|
137
|
+
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length, heading-order-invalid can be deployed. This suggestion has checkType: undefined',
|
|
120
138
|
});
|
|
121
139
|
});
|
|
122
140
|
|
|
@@ -129,7 +147,7 @@ describe('HeadingsMapper', () => {
|
|
|
129
147
|
|
|
130
148
|
expect(result).to.deep.equal({
|
|
131
149
|
eligible: false,
|
|
132
|
-
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length can be deployed. This suggestion has checkType: undefined',
|
|
150
|
+
reason: 'Only heading-empty, heading-missing-h1, heading-h1-length, heading-order-invalid can be deployed. This suggestion has checkType: undefined',
|
|
133
151
|
});
|
|
134
152
|
});
|
|
135
153
|
|
|
@@ -231,6 +249,68 @@ describe('HeadingsMapper', () => {
|
|
|
231
249
|
reason: 'transformRules.action must be replace for heading-h1-length',
|
|
232
250
|
});
|
|
233
251
|
});
|
|
252
|
+
|
|
253
|
+
it('should return ineligible for heading-order-invalid with invalid action', () => {
|
|
254
|
+
const suggestion = {
|
|
255
|
+
getData: () => ({
|
|
256
|
+
checkType: 'heading-order-invalid',
|
|
257
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
258
|
+
transformRules: {
|
|
259
|
+
action: 'replace',
|
|
260
|
+
selector: '.invalid-section',
|
|
261
|
+
valueFormat: 'hast',
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = mapper.canDeploy(suggestion);
|
|
267
|
+
|
|
268
|
+
expect(result).to.deep.equal({
|
|
269
|
+
eligible: false,
|
|
270
|
+
reason: 'transformRules.action must be replaceWith for heading-order-invalid',
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return ineligible for heading-order-invalid with missing valueFormat', () => {
|
|
275
|
+
const suggestion = {
|
|
276
|
+
getData: () => ({
|
|
277
|
+
checkType: 'heading-order-invalid',
|
|
278
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
279
|
+
transformRules: {
|
|
280
|
+
action: 'replaceWith',
|
|
281
|
+
selector: '.invalid-section',
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const result = mapper.canDeploy(suggestion);
|
|
287
|
+
|
|
288
|
+
expect(result).to.deep.equal({
|
|
289
|
+
eligible: false,
|
|
290
|
+
reason: 'transformRules.valueFormat must be hast for heading-order-invalid',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should return ineligible for heading-order-invalid with invalid valueFormat', () => {
|
|
295
|
+
const suggestion = {
|
|
296
|
+
getData: () => ({
|
|
297
|
+
checkType: 'heading-order-invalid',
|
|
298
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
299
|
+
transformRules: {
|
|
300
|
+
action: 'replaceWith',
|
|
301
|
+
selector: '.invalid-section',
|
|
302
|
+
valueFormat: 'text',
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = mapper.canDeploy(suggestion);
|
|
308
|
+
|
|
309
|
+
expect(result).to.deep.equal({
|
|
310
|
+
eligible: false,
|
|
311
|
+
reason: 'transformRules.valueFormat must be hast for heading-order-invalid',
|
|
312
|
+
});
|
|
313
|
+
});
|
|
234
314
|
});
|
|
235
315
|
|
|
236
316
|
describe('suggestionsToPatches', () => {
|
|
@@ -352,6 +432,77 @@ describe('HeadingsMapper', () => {
|
|
|
352
432
|
expect(patch.tag).to.be.undefined;
|
|
353
433
|
});
|
|
354
434
|
|
|
435
|
+
it('should create patch for heading-order-invalid with transformRules', () => {
|
|
436
|
+
const hastValue = {
|
|
437
|
+
type: 'element',
|
|
438
|
+
tagName: 'div',
|
|
439
|
+
children: [
|
|
440
|
+
{ type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'Section Title' }] },
|
|
441
|
+
{ type: 'element', tagName: 'h3', children: [{ type: 'text', value: 'Subsection' }] },
|
|
442
|
+
],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const suggestion = {
|
|
446
|
+
getId: () => 'sugg-101',
|
|
447
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
448
|
+
getData: () => ({
|
|
449
|
+
checkType: 'heading-order-invalid',
|
|
450
|
+
recommendedAction: hastValue,
|
|
451
|
+
transformRules: {
|
|
452
|
+
action: 'replaceWith',
|
|
453
|
+
selector: '.content-section',
|
|
454
|
+
valueFormat: 'hast',
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-101');
|
|
460
|
+
expect(patches.length).to.equal(1);
|
|
461
|
+
const patch = patches[0];
|
|
462
|
+
|
|
463
|
+
expect(patch).to.deep.include({
|
|
464
|
+
op: 'replaceWith',
|
|
465
|
+
selector: '.content-section',
|
|
466
|
+
value: hastValue,
|
|
467
|
+
opportunityId: 'opp-101',
|
|
468
|
+
suggestionId: 'sugg-101',
|
|
469
|
+
prerenderRequired: true,
|
|
470
|
+
});
|
|
471
|
+
expect(patch.lastUpdated).to.be.a('number');
|
|
472
|
+
expect(patch.tag).to.be.undefined;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should return empty array for heading-order-invalid without transformRules', () => {
|
|
476
|
+
const suggestion = {
|
|
477
|
+
getId: () => 'sugg-102',
|
|
478
|
+
getData: () => ({
|
|
479
|
+
checkType: 'heading-order-invalid',
|
|
480
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-102');
|
|
485
|
+
expect(patches.length).to.equal(0);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should return empty array for heading-order-invalid with invalid action', () => {
|
|
489
|
+
const suggestion = {
|
|
490
|
+
getId: () => 'sugg-103',
|
|
491
|
+
getData: () => ({
|
|
492
|
+
checkType: 'heading-order-invalid',
|
|
493
|
+
recommendedAction: { type: 'element', tagName: 'h2', children: [] },
|
|
494
|
+
transformRules: {
|
|
495
|
+
action: 'replace',
|
|
496
|
+
selector: '.content-section',
|
|
497
|
+
valueFormat: 'hast',
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-103');
|
|
503
|
+
expect(patches.length).to.equal(0);
|
|
504
|
+
});
|
|
505
|
+
|
|
355
506
|
it('should return empty array for heading-missing-h1 without transformRules', () => {
|
|
356
507
|
const suggestion = {
|
|
357
508
|
getId: () => 'sugg-999',
|
|
@@ -0,0 +1,216 @@
|
|
|
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 PrerenderMapper from '../../src/mappers/prerender-mapper.js';
|
|
17
|
+
|
|
18
|
+
describe('PrerenderMapper', () => {
|
|
19
|
+
let mapper;
|
|
20
|
+
let log;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
log = {
|
|
24
|
+
debug: () => {},
|
|
25
|
+
info: () => {},
|
|
26
|
+
warn: () => {},
|
|
27
|
+
error: () => {},
|
|
28
|
+
};
|
|
29
|
+
mapper = new PrerenderMapper(log);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getOpportunityType', () => {
|
|
33
|
+
it('should return prerender', () => {
|
|
34
|
+
expect(mapper.getOpportunityType()).to.equal('prerender');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('requiresPrerender', () => {
|
|
39
|
+
it('should return true', () => {
|
|
40
|
+
expect(mapper.requiresPrerender()).to.be.true;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('allowConfigsWithoutPatch', () => {
|
|
45
|
+
it('should return true for prerender mapper', () => {
|
|
46
|
+
expect(mapper.allowConfigsWithoutPatch()).to.be.true;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('suggestionsToPatches', () => {
|
|
51
|
+
it('should return empty array for prerender suggestions', () => {
|
|
52
|
+
const suggestion = {
|
|
53
|
+
getId: () => 'test-suggestion-id',
|
|
54
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
55
|
+
getData: () => ({
|
|
56
|
+
url: 'https://example.com/page',
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const patches = mapper.suggestionsToPatches(
|
|
61
|
+
'/page',
|
|
62
|
+
[suggestion],
|
|
63
|
+
'test-opportunity-id',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(patches).to.be.an('array');
|
|
67
|
+
expect(patches).to.be.empty;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return empty array even with multiple suggestions', () => {
|
|
71
|
+
const suggestions = [
|
|
72
|
+
{
|
|
73
|
+
getId: () => 'suggestion-1',
|
|
74
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
75
|
+
getData: () => ({
|
|
76
|
+
url: 'https://example.com/page1',
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
getId: () => 'suggestion-2',
|
|
81
|
+
getUpdatedAt: () => '2025-01-15T11:00:00.000Z',
|
|
82
|
+
getData: () => ({
|
|
83
|
+
url: 'https://example.com/page2',
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const patches = mapper.suggestionsToPatches(
|
|
89
|
+
'/page',
|
|
90
|
+
suggestions,
|
|
91
|
+
'test-opportunity-id',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(patches).to.be.an('array');
|
|
95
|
+
expect(patches).to.be.empty;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('canDeploy', () => {
|
|
100
|
+
it('should return eligible for valid suggestion with URL', () => {
|
|
101
|
+
const suggestion = {
|
|
102
|
+
getData: () => ({
|
|
103
|
+
url: 'https://example.com/page',
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = mapper.canDeploy(suggestion);
|
|
108
|
+
|
|
109
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return ineligible when URL is missing', () => {
|
|
113
|
+
const suggestion = {
|
|
114
|
+
getData: () => ({}),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = mapper.canDeploy(suggestion);
|
|
118
|
+
|
|
119
|
+
expect(result).to.deep.equal({
|
|
120
|
+
eligible: false,
|
|
121
|
+
reason: 'url is required',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return ineligible when URL is empty string', () => {
|
|
126
|
+
const suggestion = {
|
|
127
|
+
getData: () => ({
|
|
128
|
+
url: '',
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = mapper.canDeploy(suggestion);
|
|
133
|
+
|
|
134
|
+
expect(result).to.deep.equal({
|
|
135
|
+
eligible: false,
|
|
136
|
+
reason: 'url is required',
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return ineligible when URL is null', () => {
|
|
141
|
+
const suggestion = {
|
|
142
|
+
getData: () => ({
|
|
143
|
+
url: null,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = mapper.canDeploy(suggestion);
|
|
148
|
+
|
|
149
|
+
expect(result).to.deep.equal({
|
|
150
|
+
eligible: false,
|
|
151
|
+
reason: 'url is required',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return ineligible when URL is undefined', () => {
|
|
156
|
+
const suggestion = {
|
|
157
|
+
getData: () => ({
|
|
158
|
+
url: undefined,
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = mapper.canDeploy(suggestion);
|
|
163
|
+
|
|
164
|
+
expect(result).to.deep.equal({
|
|
165
|
+
eligible: false,
|
|
166
|
+
reason: 'url is required',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return ineligible when data is null', () => {
|
|
171
|
+
const suggestion = {
|
|
172
|
+
getData: () => null,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = mapper.canDeploy(suggestion);
|
|
176
|
+
|
|
177
|
+
expect(result).to.deep.equal({
|
|
178
|
+
eligible: false,
|
|
179
|
+
reason: 'url is required',
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return ineligible when data is undefined', () => {
|
|
184
|
+
const suggestion = {
|
|
185
|
+
getData: () => undefined,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = mapper.canDeploy(suggestion);
|
|
189
|
+
|
|
190
|
+
expect(result).to.deep.equal({
|
|
191
|
+
eligible: false,
|
|
192
|
+
reason: 'url is required',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should return eligible for various valid URL formats', () => {
|
|
197
|
+
const urls = [
|
|
198
|
+
'https://example.com',
|
|
199
|
+
'https://example.com/path',
|
|
200
|
+
'https://subdomain.example.com/path/to/page',
|
|
201
|
+
'https://example.com/path?query=value',
|
|
202
|
+
'https://example.com/path#hash',
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
urls.forEach((url) => {
|
|
206
|
+
const suggestion = {
|
|
207
|
+
getData: () => ({ url }),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = mapper.canDeploy(suggestion);
|
|
211
|
+
|
|
212
|
+
expect(result).to.deep.equal({ eligible: true });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|