@sudobility/testomniac_runner_service 0.1.51 → 0.1.52
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/dist/analyzer/page-analyzer.d.ts +11 -0
- package/dist/analyzer/page-analyzer.d.ts.map +1 -1
- package/dist/analyzer/page-analyzer.js +394 -44
- package/dist/analyzer/page-analyzer.js.map +1 -1
- package/dist/expertise/tester/commerce-checks.d.ts.map +1 -1
- package/dist/expertise/tester/commerce-checks.js +79 -0
- package/dist/expertise/tester/commerce-checks.js.map +1 -1
- package/dist/expertise/tester/dialog-feedback-checks.d.ts.map +1 -1
- package/dist/expertise/tester/dialog-feedback-checks.js +27 -10
- package/dist/expertise/tester/dialog-feedback-checks.js.map +1 -1
- package/dist/expertise/tester/form-checks.d.ts +6 -0
- package/dist/expertise/tester/form-checks.d.ts.map +1 -1
- package/dist/expertise/tester/form-checks.js +50 -0
- package/dist/expertise/tester/form-checks.js.map +1 -1
- package/dist/expertise/tester/keyboard-disclosure-checks.d.ts +4 -0
- package/dist/expertise/tester/keyboard-disclosure-checks.d.ts.map +1 -1
- package/dist/expertise/tester/keyboard-disclosure-checks.js +30 -0
- package/dist/expertise/tester/keyboard-disclosure-checks.js.map +1 -1
- package/dist/expertise/tester/list-workflow-checks.js +12 -2
- package/dist/expertise/tester/list-workflow-checks.js.map +1 -1
- package/dist/expertise/tester/search-checks.d.ts.map +1 -1
- package/dist/expertise/tester/search-checks.js +23 -0
- package/dist/expertise/tester/search-checks.js.map +1 -1
- package/dist/expertise/tester/variant-checks.d.ts.map +1 -1
- package/dist/expertise/tester/variant-checks.js +36 -0
- package/dist/expertise/tester/variant-checks.js.map +1 -1
- package/dist/expertise/tester-expertise.d.ts.map +1 -1
- package/dist/expertise/tester-expertise.js +8 -2
- package/dist/expertise/tester-expertise.js.map +1 -1
- package/package.json +1 -1
|
@@ -720,6 +720,16 @@ export class PageAnalyzer {
|
|
|
720
720
|
steps,
|
|
721
721
|
globalExpectations: [
|
|
722
722
|
...this.defaultFlowExpectations(`Form ${formLabel} should execute cleanly`),
|
|
723
|
+
...(formType === "login" || formType === "signup"
|
|
724
|
+
? [
|
|
725
|
+
this.makeExpectation(ExpectationType.NavigationOrStateChanged, `${formLabel} should advance authentication state after a successful submit`, {
|
|
726
|
+
severity: ExpectationSeverity.ShouldPass,
|
|
727
|
+
}),
|
|
728
|
+
this.makeExpectation(ExpectationType.ErrorStateCleared, `${formLabel} should not leave a visible error state after a successful authentication submit`, {
|
|
729
|
+
severity: ExpectationSeverity.ShouldPass,
|
|
730
|
+
}),
|
|
731
|
+
]
|
|
732
|
+
: []),
|
|
723
733
|
this.makeExpectation(ExpectationType.FormSubmittedSuccessfully, `Form ${formLabel} should submit without client-side errors`),
|
|
724
734
|
this.makeExpectation(ExpectationType.NetworkRequestMade, `Submitting ${formLabel} should trigger a backend mutation request`, {
|
|
725
735
|
expectedValue: "mutation",
|
|
@@ -742,7 +752,7 @@ export class PageAnalyzer {
|
|
|
742
752
|
};
|
|
743
753
|
}
|
|
744
754
|
buildNegativeFormTestElement(form, formLabel, formType, omittedField, currentPath, sizeClass, uid, startingPageStateId, validValues) {
|
|
745
|
-
const steps = this.buildFormSteps(form, validValues, omittedField.selector);
|
|
755
|
+
const steps = this.buildFormSteps(form, validValues, omittedField.selector, this.buildValuePreservationExpectations(form, validValues, omittedField.selector));
|
|
746
756
|
return {
|
|
747
757
|
title: `Form Negative — ${formLabel} (missing ${this.fieldLabel(omittedField)})`,
|
|
748
758
|
type: "form_negative",
|
|
@@ -755,6 +765,7 @@ export class PageAnalyzer {
|
|
|
755
765
|
globalExpectations: [
|
|
756
766
|
...this.defaultFlowExpectations(`Negative form check for ${formLabel}`),
|
|
757
767
|
this.makeExpectation(ExpectationType.ValidationMessageVisible, `Validation feedback should appear when ${this.fieldLabel(omittedField)} is omitted`),
|
|
768
|
+
this.makeExpectation(ExpectationType.ErrorStateVisible, `Omitting ${this.fieldLabel(omittedField)} should surface an error state`),
|
|
758
769
|
this.makeExpectation("required_error_shown_for_field", `${this.fieldLabel(omittedField)} should show a required-field error when omitted`, {
|
|
759
770
|
targetPath: omittedField.selector,
|
|
760
771
|
}),
|
|
@@ -763,7 +774,7 @@ export class PageAnalyzer {
|
|
|
763
774
|
};
|
|
764
775
|
}
|
|
765
776
|
buildFormCorrectionTestElement(form, formLabel, formType, correctedField, currentPath, sizeClass, uid, startingPageStateId, validValues) {
|
|
766
|
-
const steps = this.buildFormSteps(form, validValues, correctedField.selector);
|
|
777
|
+
const steps = this.buildFormSteps(form, validValues, correctedField.selector, this.buildValuePreservationExpectations(form, validValues, correctedField.selector));
|
|
767
778
|
const correctionValue = validValues[correctedField.selector];
|
|
768
779
|
if (correctionValue) {
|
|
769
780
|
const correctionStep = this.buildFieldStep(correctedField, correctionValue, this.makeExpectation("field_error_clears_after_fix", `${this.fieldLabel(correctedField)} should clear its validation error after correction`, {
|
|
@@ -785,6 +796,7 @@ export class PageAnalyzer {
|
|
|
785
796
|
steps,
|
|
786
797
|
globalExpectations: [
|
|
787
798
|
...this.defaultFlowExpectations(`Correction flow for ${formLabel}`),
|
|
799
|
+
this.makeExpectation(ExpectationType.ErrorStateCleared, `Error state for ${formLabel} should clear after correcting ${this.fieldLabel(correctedField)}`),
|
|
788
800
|
this.makeExpectation(ExpectationType.FormSubmittedSuccessfully, `Form ${formLabel} should submit after correcting ${this.fieldLabel(correctedField)}`),
|
|
789
801
|
this.makeExpectation(ExpectationType.NetworkRequestMade, `Submitting ${formLabel} after correction should trigger a backend mutation request`, {
|
|
790
802
|
expectedValue: "mutation",
|
|
@@ -858,7 +870,7 @@ export class PageAnalyzer {
|
|
|
858
870
|
uid,
|
|
859
871
|
};
|
|
860
872
|
}
|
|
861
|
-
buildFormSteps(form, valuesBySelector, omittedSelector) {
|
|
873
|
+
buildFormSteps(form, valuesBySelector, omittedSelector, postSubmitExpectations = []) {
|
|
862
874
|
const steps = [];
|
|
863
875
|
for (const field of form.fields) {
|
|
864
876
|
if (field.selector === omittedSelector)
|
|
@@ -876,7 +888,7 @@ export class PageAnalyzer {
|
|
|
876
888
|
if (step)
|
|
877
889
|
steps.push(step);
|
|
878
890
|
}
|
|
879
|
-
steps.push(...this.buildSubmitSteps(form.submitSelector));
|
|
891
|
+
steps.push(...this.buildSubmitSteps(form.submitSelector, postSubmitExpectations));
|
|
880
892
|
return steps;
|
|
881
893
|
}
|
|
882
894
|
buildFieldStep(field, value, trailingExpectation) {
|
|
@@ -944,7 +956,7 @@ export class PageAnalyzer {
|
|
|
944
956
|
continueOnFailure: false,
|
|
945
957
|
};
|
|
946
958
|
}
|
|
947
|
-
buildSubmitSteps(submitSelector) {
|
|
959
|
+
buildSubmitSteps(submitSelector, postSubmitExpectations = []) {
|
|
948
960
|
if (!submitSelector)
|
|
949
961
|
return [];
|
|
950
962
|
return [
|
|
@@ -965,12 +977,39 @@ export class PageAnalyzer {
|
|
|
965
977
|
playwrightCode: "await page.waitForLoadState('networkidle')",
|
|
966
978
|
description: "Wait for post-submit state",
|
|
967
979
|
},
|
|
968
|
-
expectations:
|
|
980
|
+
expectations: postSubmitExpectations,
|
|
969
981
|
description: "Wait for post-submit state",
|
|
970
982
|
continueOnFailure: true,
|
|
971
983
|
},
|
|
972
984
|
];
|
|
973
985
|
}
|
|
986
|
+
buildValuePreservationExpectations(form, valuesBySelector, omittedSelector) {
|
|
987
|
+
const expectations = [];
|
|
988
|
+
for (const field of form.fields) {
|
|
989
|
+
if (field.selector === omittedSelector)
|
|
990
|
+
continue;
|
|
991
|
+
const analyzerField = field;
|
|
992
|
+
const fieldIsImmutable = Boolean(analyzerField.disabled ||
|
|
993
|
+
analyzerField.readOnly ||
|
|
994
|
+
this.looksVisuallyDisabledField(analyzerField));
|
|
995
|
+
if (fieldIsImmutable)
|
|
996
|
+
continue;
|
|
997
|
+
const value = valuesBySelector[field.selector];
|
|
998
|
+
if (!value || this.isSkippableFieldType(field))
|
|
999
|
+
continue;
|
|
1000
|
+
if (this.isCheckboxLike(field)) {
|
|
1001
|
+
expectations.push(this.makeExpectation(ExpectationType.ElementChecked, `${this.fieldLabel(field)} should preserve its selected state after validation feedback`, {
|
|
1002
|
+
targetPath: field.selector,
|
|
1003
|
+
}));
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
expectations.push(this.makeExpectation(ExpectationType.InputValue, `${this.fieldLabel(field)} should preserve its value after validation feedback`, {
|
|
1007
|
+
targetPath: field.selector,
|
|
1008
|
+
expectedValue: value,
|
|
1009
|
+
}));
|
|
1010
|
+
}
|
|
1011
|
+
return expectations;
|
|
1012
|
+
}
|
|
974
1013
|
planFormValues(form, actionableItems) {
|
|
975
1014
|
const values = {};
|
|
976
1015
|
for (const field of form.fields) {
|
|
@@ -1132,6 +1171,29 @@ export class PageAnalyzer {
|
|
|
1132
1171
|
],
|
|
1133
1172
|
uid,
|
|
1134
1173
|
},
|
|
1174
|
+
{
|
|
1175
|
+
title: `Search Recovery — ${formLabel}`,
|
|
1176
|
+
type: "form",
|
|
1177
|
+
sizeClass,
|
|
1178
|
+
surface_tags: ["form", "search", "recovery"],
|
|
1179
|
+
priority: 3,
|
|
1180
|
+
startingPageStateId,
|
|
1181
|
+
startingPath: currentPath,
|
|
1182
|
+
steps: [
|
|
1183
|
+
...this.buildFormSteps(form, noResultsValues, undefined),
|
|
1184
|
+
...this.buildSearchRecoverySteps(form, searchField, "test"),
|
|
1185
|
+
],
|
|
1186
|
+
globalExpectations: [
|
|
1187
|
+
...this.defaultFlowExpectations(`Search recovery flow ${formLabel}`),
|
|
1188
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, `Recovering search results via ${formLabel} should issue GET requests`, {
|
|
1189
|
+
expectedValue: "GET",
|
|
1190
|
+
timeoutMs: 3000,
|
|
1191
|
+
expectedTextTokens: ["search", "q=", "query", "term"],
|
|
1192
|
+
}),
|
|
1193
|
+
this.makeExpectation(ExpectationType.LoadingCompletes, `Search recovery for ${formLabel} should finish loading`),
|
|
1194
|
+
],
|
|
1195
|
+
uid,
|
|
1196
|
+
},
|
|
1135
1197
|
];
|
|
1136
1198
|
const clearAction = actionableItems.find(item => this.isSearchClearItem(item));
|
|
1137
1199
|
if (clearAction) {
|
|
@@ -1161,6 +1223,22 @@ export class PageAnalyzer {
|
|
|
1161
1223
|
}
|
|
1162
1224
|
return tests;
|
|
1163
1225
|
}
|
|
1226
|
+
buildSearchRecoverySteps(form, searchField, recoveryValue) {
|
|
1227
|
+
const steps = [];
|
|
1228
|
+
const refillStep = this.buildFieldStep(searchField, recoveryValue);
|
|
1229
|
+
if (refillStep) {
|
|
1230
|
+
steps.push(refillStep);
|
|
1231
|
+
}
|
|
1232
|
+
steps.push(...this.buildSubmitSteps(form.submitSelector, [
|
|
1233
|
+
this.makeExpectation(ExpectationType.ResultsChanged, `${this.fieldLabel(searchField)} should recover from the empty state to a different result set`),
|
|
1234
|
+
this.makeExpectation(ExpectationType.InputValue, `${this.fieldLabel(searchField)} should preserve the recovery query after resubmission`, {
|
|
1235
|
+
targetPath: searchField.selector,
|
|
1236
|
+
expectedValue: recoveryValue,
|
|
1237
|
+
}),
|
|
1238
|
+
this.makeExpectation(ExpectationType.LoadingCompletes, "Recovered search results should finish loading"),
|
|
1239
|
+
]));
|
|
1240
|
+
return steps;
|
|
1241
|
+
}
|
|
1164
1242
|
describeForm(form, index) {
|
|
1165
1243
|
const namedField = form.fields.find(field => field.label || field.name);
|
|
1166
1244
|
const descriptor = namedField?.label || namedField?.name || `form ${index + 1}`;
|
|
@@ -1169,8 +1247,10 @@ export class PageAnalyzer {
|
|
|
1169
1247
|
buildSemanticJourneyTestElements(context) {
|
|
1170
1248
|
const items = context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector));
|
|
1171
1249
|
const journeys = [];
|
|
1250
|
+
const collectionCount = this.estimateCollectionCount(context.html);
|
|
1172
1251
|
const addToCart = items.find(item => this.isAddToCartItem(item));
|
|
1173
1252
|
const checkout = items.find(item => this.isCheckoutItem(item));
|
|
1253
|
+
const createCollectionAction = items.find(item => this.isCreateCollectionAction(item));
|
|
1174
1254
|
if (addToCart && checkout) {
|
|
1175
1255
|
journeys.push(this.buildJourneyTestElement("Commerce journey", ["commerce", "cart", "checkout"], context, [
|
|
1176
1256
|
this.buildJourneyAction(addToCart, "Add item to cart", [
|
|
@@ -1207,29 +1287,63 @@ export class PageAnalyzer {
|
|
|
1207
1287
|
}
|
|
1208
1288
|
const removeItem = items.find(item => this.isRemoveItemAction(item));
|
|
1209
1289
|
if (removeItem) {
|
|
1290
|
+
const removeExpectations = [
|
|
1291
|
+
this.makeExpectation("navigation_or_state_changed", "Removing an item should change the page state"),
|
|
1292
|
+
this.makeExpectation("count_changed", "Removing an item should update a visible count", {
|
|
1293
|
+
expectedCountDelta: -1,
|
|
1294
|
+
}),
|
|
1295
|
+
this.makeExpectation("cart_summary_changed", "Removing an item should update the cart summary"),
|
|
1296
|
+
this.makeExpectation("row_count_changed", "Removing an item should change the visible row or item count", {
|
|
1297
|
+
expectedCountDelta: -1,
|
|
1298
|
+
}),
|
|
1299
|
+
this.makeExpectation("results_changed", "Removing an item should change the visible collection state"),
|
|
1300
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, "Removing an item should trigger a backend mutation request", {
|
|
1301
|
+
expectedValue: "mutation",
|
|
1302
|
+
timeoutMs: 3000,
|
|
1303
|
+
}),
|
|
1304
|
+
this.makeExpectation(ExpectationType.NoDuplicateMutationRequests, "Removing an item should not trigger duplicate mutation requests"),
|
|
1305
|
+
this.makeExpectation("feedback_visible", "Removing an item should provide visible feedback", {
|
|
1306
|
+
expectedTextTokens: ["removed", "updated", "cart", "bag"],
|
|
1307
|
+
forbiddenTextTokens: ["error", "failed"],
|
|
1308
|
+
}),
|
|
1309
|
+
this.makeExpectation("feedback_not_duplicated", "Removing an item should not show duplicate feedback messages"),
|
|
1310
|
+
this.makeExpectation("loading_completes", "Removal flow should complete loading"),
|
|
1311
|
+
this.makeExpectation("page_responsive", "Page should remain responsive after removal"),
|
|
1312
|
+
];
|
|
1313
|
+
if (this.estimateCollectionCount(context.html) <= 1) {
|
|
1314
|
+
removeExpectations.push(this.makeExpectation(ExpectationType.EmptyStateVisible, "Removing the last visible item should show an empty state or zero-results message", {
|
|
1315
|
+
expectedTextTokens: [
|
|
1316
|
+
"no items",
|
|
1317
|
+
"no products",
|
|
1318
|
+
"empty",
|
|
1319
|
+
"no results",
|
|
1320
|
+
"nothing found",
|
|
1321
|
+
],
|
|
1322
|
+
severity: ExpectationSeverity.ShouldPass,
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1210
1325
|
journeys.push(this.buildJourneyTestElement("Remove item from collection", ["commerce", "remove"], context, [
|
|
1211
|
-
this.buildJourneyAction(removeItem, "Remove item",
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
]),
|
|
1326
|
+
this.buildJourneyAction(removeItem, "Remove item", removeExpectations),
|
|
1327
|
+
]));
|
|
1328
|
+
}
|
|
1329
|
+
if (createCollectionAction) {
|
|
1330
|
+
const createExpectations = [
|
|
1331
|
+
this.makeExpectation("navigation_or_state_changed", "Creating or adding a record should change the page state"),
|
|
1332
|
+
this.makeExpectation("row_count_changed", "Creating or adding a record should increase the visible row or item count", {
|
|
1333
|
+
expectedCountDelta: 1,
|
|
1334
|
+
}),
|
|
1335
|
+
this.makeExpectation("results_changed", "Creating or adding a record should change the visible collection state"),
|
|
1336
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, "Creating or adding a record should trigger a backend mutation request", {
|
|
1337
|
+
expectedValue: "mutation",
|
|
1338
|
+
timeoutMs: 3000,
|
|
1339
|
+
}),
|
|
1340
|
+
this.makeExpectation(ExpectationType.NoDuplicateMutationRequests, "Creating or adding a record should not trigger duplicate mutation requests"),
|
|
1341
|
+
this.makeExpectation("feedback_not_duplicated", "Creating or adding a record should not duplicate visible feedback"),
|
|
1342
|
+
this.makeExpectation("loading_completes", "Create/add flow should complete loading"),
|
|
1343
|
+
this.makeExpectation("page_responsive", "Page should remain responsive after creating or adding a record"),
|
|
1344
|
+
];
|
|
1345
|
+
journeys.push(this.buildJourneyTestElement("Create or add collection record", ["list", "crud", "create"], context, [
|
|
1346
|
+
this.buildJourneyAction(createCollectionAction, "Create or add record", createExpectations),
|
|
1233
1347
|
]));
|
|
1234
1348
|
}
|
|
1235
1349
|
const authEntry = items.find(item => this.isAuthEntryItem(item));
|
|
@@ -1242,6 +1356,51 @@ export class PageAnalyzer {
|
|
|
1242
1356
|
]),
|
|
1243
1357
|
]));
|
|
1244
1358
|
}
|
|
1359
|
+
const protectedAction = items.find(item => item !== authEntry && this.isProtectedActionItem(item));
|
|
1360
|
+
if (authEntry && protectedAction) {
|
|
1361
|
+
journeys.push(this.buildJourneyTestElement("Protected action auth gate journey", ["auth", "protected"], context, [
|
|
1362
|
+
this.buildJourneyAction(protectedAction, "Open protected action", [
|
|
1363
|
+
this.makeExpectation(ExpectationType.NavigationOrStateChanged, "Protected action should open a gated state, redirect, or login requirement"),
|
|
1364
|
+
this.makeExpectation(ExpectationType.PageResponsive, "Page should remain responsive when auth gating is triggered"),
|
|
1365
|
+
this.makeExpectation(ExpectationType.LoadingCompletes, "Protected action gate should settle cleanly"),
|
|
1366
|
+
]),
|
|
1367
|
+
]));
|
|
1368
|
+
}
|
|
1369
|
+
const logoutAction = items.find(item => this.isLogoutAction(item));
|
|
1370
|
+
if (logoutAction) {
|
|
1371
|
+
journeys.push(this.buildJourneyTestElement("Logout journey", ["auth", "logout"], context, [
|
|
1372
|
+
this.buildJourneyAction(logoutAction, "Log out", [
|
|
1373
|
+
this.makeExpectation(ExpectationType.NavigationOrStateChanged, "Logging out should change application state"),
|
|
1374
|
+
this.makeExpectation(ExpectationType.PageResponsive, "Page should remain responsive during logout"),
|
|
1375
|
+
this.makeExpectation(ExpectationType.LoadingCompletes, "Logout should settle cleanly"),
|
|
1376
|
+
this.makeExpectation(ExpectationType.FeedbackVisible, "Logout should provide visible confirmation or a clear state change", {
|
|
1377
|
+
forbiddenTextTokens: ["error", "failed"],
|
|
1378
|
+
}),
|
|
1379
|
+
this.makeExpectation(ExpectationType.ErrorStateCleared, "Logout should not leave visible recoverable error state behind", {
|
|
1380
|
+
severity: ExpectationSeverity.ShouldPass,
|
|
1381
|
+
}),
|
|
1382
|
+
]),
|
|
1383
|
+
]));
|
|
1384
|
+
}
|
|
1385
|
+
const retryAction = items.find(item => this.isRetryAction(item));
|
|
1386
|
+
if (retryAction) {
|
|
1387
|
+
journeys.push(this.buildJourneyTestElement("Retry recovery journey", ["recovery", "retry"], context, [
|
|
1388
|
+
this.buildJourneyAction(retryAction, "Retry failed action", [
|
|
1389
|
+
this.makeExpectation(ExpectationType.ErrorStateCleared, "Retrying should clear any visible recoverable error state"),
|
|
1390
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, "Retrying should trigger a follow-up request or state transition", {
|
|
1391
|
+
expectedValue: "ANY",
|
|
1392
|
+
timeoutMs: 3000,
|
|
1393
|
+
}),
|
|
1394
|
+
this.makeExpectation("navigation_or_state_changed", "Retrying should change page state or visibly advance recovery"),
|
|
1395
|
+
this.makeExpectation("loading_completes", "Retry recovery should complete loading"),
|
|
1396
|
+
this.makeExpectation("page_responsive", "Page should remain responsive during retry recovery"),
|
|
1397
|
+
this.makeExpectation("feedback_not_duplicated", "Retry recovery should not duplicate visible feedback"),
|
|
1398
|
+
this.makeExpectation("feedback_visible", "Retry recovery should show updated feedback or state confirmation", {
|
|
1399
|
+
forbiddenTextTokens: ["error", "failed", "try again"],
|
|
1400
|
+
}),
|
|
1401
|
+
]),
|
|
1402
|
+
]));
|
|
1403
|
+
}
|
|
1245
1404
|
const mediaCandidate = items.find(item => this.isMediaOpenItem(item));
|
|
1246
1405
|
if (mediaCandidate) {
|
|
1247
1406
|
const mediaExpectations = [
|
|
@@ -1255,19 +1414,33 @@ export class PageAnalyzer {
|
|
|
1255
1414
|
this.buildJourneyAction(mediaCandidate, "Open media", mediaExpectations),
|
|
1256
1415
|
]));
|
|
1257
1416
|
}
|
|
1258
|
-
const quantityAction
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
this.
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1417
|
+
for (const quantityAction of items.filter(item => this.isQuantityAction(item))) {
|
|
1418
|
+
const quantityDelta = this.inferQuantityDelta(quantityAction);
|
|
1419
|
+
const quantityExpectations = [
|
|
1420
|
+
this.makeExpectation("count_changed", "Adjusting quantity should update a visible count or quantity indicator", quantityDelta == null
|
|
1421
|
+
? undefined
|
|
1422
|
+
: { expectedCountDelta: quantityDelta }),
|
|
1423
|
+
this.makeExpectation("cart_summary_changed", "Adjusting quantity should update subtotal, totals, or line pricing"),
|
|
1424
|
+
this.makeExpectation("results_changed", "Adjusting quantity should change the visible cart or line-item state"),
|
|
1425
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, "Adjusting quantity should trigger a backend mutation request", {
|
|
1426
|
+
expectedValue: "mutation",
|
|
1427
|
+
timeoutMs: 3000,
|
|
1428
|
+
}),
|
|
1429
|
+
this.makeExpectation(ExpectationType.NoDuplicateMutationRequests, "Adjusting quantity should not trigger duplicate mutation requests"),
|
|
1430
|
+
this.makeExpectation("feedback_not_duplicated", "Quantity adjustment should not duplicate visible feedback"),
|
|
1431
|
+
this.makeExpectation("loading_completes", "Quantity adjustment should complete loading"),
|
|
1432
|
+
this.makeExpectation("page_responsive", "Page should remain responsive during quantity adjustment"),
|
|
1433
|
+
].filter(Boolean);
|
|
1434
|
+
journeys.push(this.buildJourneyTestElement(quantityDelta === -1
|
|
1435
|
+
? "Quantity decrease journey"
|
|
1436
|
+
: quantityDelta === 1
|
|
1437
|
+
? "Quantity increase journey"
|
|
1438
|
+
: "Quantity adjustment journey", ["commerce", "quantity"], context, [
|
|
1439
|
+
this.buildJourneyAction(quantityAction, quantityDelta === -1
|
|
1440
|
+
? "Decrease quantity"
|
|
1441
|
+
: quantityDelta === 1
|
|
1442
|
+
? "Increase quantity"
|
|
1443
|
+
: "Adjust quantity", quantityExpectations),
|
|
1271
1444
|
]));
|
|
1272
1445
|
}
|
|
1273
1446
|
const filterAction = items.find(item => this.isFilterAction(item));
|
|
@@ -1306,7 +1479,9 @@ export class PageAnalyzer {
|
|
|
1306
1479
|
journeys.push(this.buildJourneyTestElement("Pagination journey", ["list", "pagination"], context, [
|
|
1307
1480
|
this.buildJourneyAction(paginationAction, "Paginate list", [
|
|
1308
1481
|
this.makeExpectation(ExpectationType.NavigationOrStateChanged, "Pagination should change the visible list state"),
|
|
1482
|
+
this.makeExpectation("results_changed", "Pagination should change the visible collection contents"),
|
|
1309
1483
|
this.makeExpectation("row_count_changed", "Pagination should change the visible rows or items"),
|
|
1484
|
+
this.makeExpectation("collection_order_changed", "Pagination should change the visible collection ordering or composition"),
|
|
1310
1485
|
this.makeExpectation(ExpectationType.LoadingCompletes, "Pagination should complete loading"),
|
|
1311
1486
|
]),
|
|
1312
1487
|
]));
|
|
@@ -1330,6 +1505,24 @@ export class PageAnalyzer {
|
|
|
1330
1505
|
]),
|
|
1331
1506
|
]));
|
|
1332
1507
|
}
|
|
1508
|
+
if (collectionCount > 0 && removeItem && createCollectionAction) {
|
|
1509
|
+
journeys.push(this.buildJourneyTestElement("Collection mutation recovery journey", ["list", "crud", "recovery"], context, [
|
|
1510
|
+
this.buildJourneyAction(removeItem, "Remove record", [
|
|
1511
|
+
this.makeExpectation("row_count_changed", "Removing a record should change the visible collection", {
|
|
1512
|
+
expectedCountDelta: -1,
|
|
1513
|
+
}),
|
|
1514
|
+
this.makeExpectation("results_changed", "Removing a record should change the visible collection contents"),
|
|
1515
|
+
]),
|
|
1516
|
+
this.waitStep(500, "Wait for collection mutation"),
|
|
1517
|
+
this.buildJourneyAction(createCollectionAction, "Add record back", [
|
|
1518
|
+
this.makeExpectation("row_count_changed", "Adding a record back should restore visible collection size", {
|
|
1519
|
+
expectedCountDelta: 1,
|
|
1520
|
+
}),
|
|
1521
|
+
this.makeExpectation("results_changed", "Adding a record back should change the visible collection contents again"),
|
|
1522
|
+
this.makeExpectation("loading_completes", "Collection recovery should complete loading"),
|
|
1523
|
+
]),
|
|
1524
|
+
]));
|
|
1525
|
+
}
|
|
1333
1526
|
const backCandidate = items.find(item => item.actionKind === "navigate" ||
|
|
1334
1527
|
this.isMediaOpenItem(item) ||
|
|
1335
1528
|
this.isAuthEntryItem(item));
|
|
@@ -1504,9 +1697,23 @@ export class PageAnalyzer {
|
|
|
1504
1697
|
!item.disabled &&
|
|
1505
1698
|
Boolean(item.selector) &&
|
|
1506
1699
|
this.isVariantSelector(item));
|
|
1507
|
-
|
|
1700
|
+
const tests = items
|
|
1508
1701
|
.map(item => this.buildVariantTestElement(item, context))
|
|
1509
1702
|
.filter((item) => Boolean(item));
|
|
1703
|
+
const purchaseAction = context.actionableItems.find(item => item.visible &&
|
|
1704
|
+
!item.disabled &&
|
|
1705
|
+
Boolean(item.selector) &&
|
|
1706
|
+
(this.isAddToCartItem(item) || this.isCheckoutItem(item)));
|
|
1707
|
+
for (const item of items) {
|
|
1708
|
+
const purchaseJourney = this.buildVariantPurchaseJourney(item, purchaseAction, context);
|
|
1709
|
+
if (purchaseJourney)
|
|
1710
|
+
tests.push(purchaseJourney);
|
|
1711
|
+
const requiredField = this.findRequiredVariantField(item, context.forms);
|
|
1712
|
+
if (requiredField && purchaseAction) {
|
|
1713
|
+
tests.push(this.buildRequiredVariantGuardTestElement(item, requiredField, purchaseAction, context));
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return tests;
|
|
1510
1717
|
}
|
|
1511
1718
|
buildVariantTestElement(item, context) {
|
|
1512
1719
|
const plannedValue = this.extractSelectableValue(item);
|
|
@@ -1548,6 +1755,110 @@ export class PageAnalyzer {
|
|
|
1548
1755
|
uid: context.uid,
|
|
1549
1756
|
};
|
|
1550
1757
|
}
|
|
1758
|
+
buildVariantPurchaseJourney(item, purchaseAction, context) {
|
|
1759
|
+
const plannedValue = this.extractSelectableValue(item);
|
|
1760
|
+
if (!plannedValue || !item.selector || !purchaseAction?.selector) {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
const label = this.describeActionableItem(item);
|
|
1764
|
+
const purchaseLabel = this.describeActionableItem(purchaseAction);
|
|
1765
|
+
const purchaseExpectations = this.isAddToCartItem(purchaseAction)
|
|
1766
|
+
? [
|
|
1767
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, `${purchaseLabel} should trigger a backend mutation request after selecting ${label}`, {
|
|
1768
|
+
expectedValue: "mutation",
|
|
1769
|
+
timeoutMs: 3000,
|
|
1770
|
+
}),
|
|
1771
|
+
this.makeExpectation(ExpectationType.NoDuplicateMutationRequests, `${purchaseLabel} should not trigger duplicate mutation requests after selecting ${label}`),
|
|
1772
|
+
this.makeExpectation(ExpectationType.CountChanged, `${purchaseLabel} should update a visible count after selecting ${label}`, {
|
|
1773
|
+
expectedCountDelta: 1,
|
|
1774
|
+
}),
|
|
1775
|
+
this.makeExpectation(ExpectationType.CartSummaryChanged, `${purchaseLabel} should update cart summary after selecting ${label}`),
|
|
1776
|
+
this.makeExpectation(ExpectationType.FeedbackVisible, `${purchaseLabel} should provide visible feedback after selecting ${label}`, {
|
|
1777
|
+
expectedTextTokens: ["added", "success", "cart", "bag"],
|
|
1778
|
+
forbiddenTextTokens: ["error", "failed"],
|
|
1779
|
+
}),
|
|
1780
|
+
]
|
|
1781
|
+
: [
|
|
1782
|
+
this.makeExpectation(ExpectationType.NetworkRequestMade, `${purchaseLabel} should trigger a request or transition after selecting ${label}`, {
|
|
1783
|
+
expectedValue: "ANY",
|
|
1784
|
+
timeoutMs: 3000,
|
|
1785
|
+
expectedTextTokens: ["checkout", "buy", "order"],
|
|
1786
|
+
}),
|
|
1787
|
+
this.makeExpectation(ExpectationType.NavigationOrStateChanged, `${purchaseLabel} should advance the purchase flow after selecting ${label}`),
|
|
1788
|
+
];
|
|
1789
|
+
return {
|
|
1790
|
+
title: `Variant purchase journey ${label}`,
|
|
1791
|
+
type: "interaction",
|
|
1792
|
+
sizeClass: context.sizeClass,
|
|
1793
|
+
surface_tags: ["variant", "purchase"],
|
|
1794
|
+
priority: 2,
|
|
1795
|
+
startingPageStateId: context.currentPageStateId,
|
|
1796
|
+
startingPath: context.currentPath,
|
|
1797
|
+
steps: [
|
|
1798
|
+
{
|
|
1799
|
+
action: {
|
|
1800
|
+
actionType: PlaywrightAction.SelectOption,
|
|
1801
|
+
path: item.selector,
|
|
1802
|
+
value: plannedValue,
|
|
1803
|
+
playwrightCode: `await page.locator('${item.selector}').selectOption('${this.escapeSingleQuotes(plannedValue)}')`,
|
|
1804
|
+
description: `Select variant ${label}`,
|
|
1805
|
+
},
|
|
1806
|
+
expectations: [
|
|
1807
|
+
this.makeExpectation(ExpectationType.InputValue, `Selecting ${label} should update the chosen option`, {
|
|
1808
|
+
targetPath: item.selector,
|
|
1809
|
+
expectedValue: plannedValue,
|
|
1810
|
+
}),
|
|
1811
|
+
this.makeExpectation(ExpectationType.VariantStateChanged, `Selecting ${label} should update product state before purchase`, {
|
|
1812
|
+
targetPath: item.selector,
|
|
1813
|
+
expectedValue: plannedValue,
|
|
1814
|
+
}),
|
|
1815
|
+
],
|
|
1816
|
+
description: `Select variant ${label}`,
|
|
1817
|
+
continueOnFailure: false,
|
|
1818
|
+
},
|
|
1819
|
+
this.buildJourneyAction(purchaseAction, `Purchase with selected ${label}`, [
|
|
1820
|
+
...purchaseExpectations,
|
|
1821
|
+
this.makeExpectation(ExpectationType.LoadingCompletes, `${purchaseLabel} should complete loading after selecting ${label}`),
|
|
1822
|
+
this.makeExpectation(ExpectationType.PageResponsive, `Page should remain responsive while completing ${purchaseLabel}`),
|
|
1823
|
+
]),
|
|
1824
|
+
],
|
|
1825
|
+
globalExpectations: this.defaultFlowExpectations(`Variant purchase journey for ${label} should execute cleanly`),
|
|
1826
|
+
uid: context.uid,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
buildRequiredVariantGuardTestElement(item, requiredField, purchaseAction, context) {
|
|
1830
|
+
const label = this.describeActionableItem(item);
|
|
1831
|
+
const purchaseLabel = this.describeActionableItem(purchaseAction);
|
|
1832
|
+
return {
|
|
1833
|
+
title: `Variant required guard ${label}`,
|
|
1834
|
+
type: "interaction",
|
|
1835
|
+
sizeClass: context.sizeClass,
|
|
1836
|
+
surface_tags: ["variant", "validation", "guard"],
|
|
1837
|
+
priority: 3,
|
|
1838
|
+
startingPageStateId: context.currentPageStateId,
|
|
1839
|
+
startingPath: context.currentPath,
|
|
1840
|
+
steps: [
|
|
1841
|
+
this.buildJourneyAction(purchaseAction, `Attempt ${purchaseLabel} without selecting ${label}`, [
|
|
1842
|
+
this.makeExpectation(ExpectationType.RequiredErrorShownForField, `${label} should show required validation before ${purchaseLabel}`, {
|
|
1843
|
+
targetPath: requiredField.selector,
|
|
1844
|
+
}),
|
|
1845
|
+
this.makeExpectation(ExpectationType.PageResponsive, `Page should remain responsive when ${purchaseLabel} is blocked by missing ${label}`),
|
|
1846
|
+
]),
|
|
1847
|
+
],
|
|
1848
|
+
globalExpectations: this.defaultFlowExpectations(`${label} should be enforced before ${purchaseLabel}`),
|
|
1849
|
+
uid: context.uid,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
findRequiredVariantField(item, forms) {
|
|
1853
|
+
for (const form of forms) {
|
|
1854
|
+
const field = form.fields.find(field => field.selector === item.selector &&
|
|
1855
|
+
field.required &&
|
|
1856
|
+
this.isSearchField(field) === false);
|
|
1857
|
+
if (field)
|
|
1858
|
+
return field;
|
|
1859
|
+
}
|
|
1860
|
+
return undefined;
|
|
1861
|
+
}
|
|
1551
1862
|
buildDisclosureToggleTestElement(item, startingPath, sizeClass, uid, startingPageStateId) {
|
|
1552
1863
|
const label = this.describeActionableItem(item);
|
|
1553
1864
|
return {
|
|
@@ -1600,7 +1911,11 @@ export class PageAnalyzer {
|
|
|
1600
1911
|
playwrightCode: `await page.locator('${item.selector}').focus()`,
|
|
1601
1912
|
description: `Focus ${label}`,
|
|
1602
1913
|
},
|
|
1603
|
-
expectations: [
|
|
1914
|
+
expectations: [
|
|
1915
|
+
this.makeExpectation(ExpectationType.ElementFocused, `${label} should be keyboard-focusable`, {
|
|
1916
|
+
targetPath: item.selector,
|
|
1917
|
+
}),
|
|
1918
|
+
],
|
|
1604
1919
|
description: `Focus ${label}`,
|
|
1605
1920
|
continueOnFailure: false,
|
|
1606
1921
|
},
|
|
@@ -1888,11 +2203,36 @@ export class PageAnalyzer {
|
|
|
1888
2203
|
return /\b(checkout|proceed to checkout|submit order|place order)\b/.test(this.semanticText(item));
|
|
1889
2204
|
}
|
|
1890
2205
|
isRemoveItemAction(item) {
|
|
1891
|
-
return /\b(remove|delete|trash|clear item|remove item)\b/.test(this.semanticText(item));
|
|
2206
|
+
return /\b(remove|delete|trash|clear item|remove item|dismiss|archive|hide item|close item)\b/.test(this.semanticText(item));
|
|
2207
|
+
}
|
|
2208
|
+
estimateCollectionCount(html) {
|
|
2209
|
+
const tableRows = (html.match(/<tr\b/gi) ?? []).length;
|
|
2210
|
+
if (tableRows > 0)
|
|
2211
|
+
return tableRows;
|
|
2212
|
+
const listItems = (html.match(/<li\b/gi) ?? []).length;
|
|
2213
|
+
if (listItems > 0)
|
|
2214
|
+
return listItems;
|
|
2215
|
+
const cards = (html.match(/<(?:article|section|div)\b[^>]*(?:product|card|result|item|row)[^>]*>/gi) ?? []).length;
|
|
2216
|
+
return cards;
|
|
1892
2217
|
}
|
|
1893
2218
|
isAuthEntryItem(item) {
|
|
1894
2219
|
return /\b(sign up|register|create account|sign in|log in|login)\b/.test(this.semanticText(item));
|
|
1895
2220
|
}
|
|
2221
|
+
isProtectedActionItem(item) {
|
|
2222
|
+
return /\b(checkout|pricing|view price|account|settings|billing|subscription|dashboard|admin|manage account|saved items|favorites|download)\b/.test(this.semanticText(item));
|
|
2223
|
+
}
|
|
2224
|
+
isLogoutAction(item) {
|
|
2225
|
+
return /\b(log out|logout|sign out)\b/.test(this.semanticText(item));
|
|
2226
|
+
}
|
|
2227
|
+
isRetryAction(item) {
|
|
2228
|
+
return /\b(retry|try again|resend|send again|reload|refresh|reconnect|resume)\b/.test(this.semanticText(item));
|
|
2229
|
+
}
|
|
2230
|
+
isCreateCollectionAction(item) {
|
|
2231
|
+
const text = this.semanticText(item);
|
|
2232
|
+
if (this.isAddToCartItem(item))
|
|
2233
|
+
return false;
|
|
2234
|
+
return /\b(add row|add record|add item|new row|new record|create item|create record|add another|new item)\b/.test(text);
|
|
2235
|
+
}
|
|
1896
2236
|
isMediaOpenItem(item) {
|
|
1897
2237
|
const text = this.semanticText(item);
|
|
1898
2238
|
return (item.tagName === "VIDEO" ||
|
|
@@ -1906,6 +2246,16 @@ export class PageAnalyzer {
|
|
|
1906
2246
|
isQuantityAction(item) {
|
|
1907
2247
|
return /\b(qty|quantity|increase|decrease|increment|decrement|plus|minus)\b/.test(this.semanticText(item));
|
|
1908
2248
|
}
|
|
2249
|
+
inferQuantityDelta(item) {
|
|
2250
|
+
const text = this.semanticText(item);
|
|
2251
|
+
if (/\b(decrease|decrement|minus|remove one|lower qty|lower quantity)\b/.test(text)) {
|
|
2252
|
+
return -1;
|
|
2253
|
+
}
|
|
2254
|
+
if (/\b(increase|increment|plus|add one|raise qty|raise quantity)\b/.test(text)) {
|
|
2255
|
+
return 1;
|
|
2256
|
+
}
|
|
2257
|
+
return undefined;
|
|
2258
|
+
}
|
|
1909
2259
|
isFilterAction(item) {
|
|
1910
2260
|
return /\b(filter|refine|apply filter|show results|category|brand|size|color|price)\b/.test(this.semanticText(item));
|
|
1911
2261
|
}
|