aria-ease 6.4.7 → 6.4.10

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/bin/cli.cjs CHANGED
@@ -488,7 +488,7 @@ ${"\u2550".repeat(60)}`);
488
488
  failureMessage,
489
489
  isOptional: test.isOptional
490
490
  };
491
- if (status === "skip" && test.requiresBrowser) {
491
+ if (status === "skip") {
492
492
  result.skipReason = "Requires real browser (addEventListener events)";
493
493
  }
494
494
  this.dynamicResults.push(result);
@@ -594,7 +594,7 @@ ${"\u2550".repeat(60)}`);
594
594
  `);
595
595
  if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
596
596
  this.log(`\u2705 All ${totalRun} tests passed!`);
597
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
597
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
598
598
  } else if (totalFailures === 0) {
599
599
  this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
600
600
  if (this.skipped > 0) {
@@ -603,7 +603,7 @@ ${"\u2550".repeat(60)}`);
603
603
  if (this.optionalSuggestions > 0) {
604
604
  this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
605
605
  }
606
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
606
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
607
607
  } else {
608
608
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
609
609
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -784,6 +784,778 @@ var init_test = __esm({
784
784
  }
785
785
  });
786
786
 
787
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
788
+ var ComboboxComponentStrategy;
789
+ var init_ComboboxComponentStrategy = __esm({
790
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
791
+ "use strict";
792
+ init_test();
793
+ ComboboxComponentStrategy = class {
794
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
795
+ this.mainSelector = mainSelector;
796
+ this.selectors = selectors;
797
+ this.actionTimeoutMs = actionTimeoutMs;
798
+ this.assertionTimeoutMs = assertionTimeoutMs;
799
+ }
800
+ async resetState(page) {
801
+ if (!this.selectors.popup) return;
802
+ const popupSelector = this.selectors.popup;
803
+ const popupElement = page.locator(popupSelector).first();
804
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
805
+ if (!isPopupVisible) return;
806
+ let menuClosed = false;
807
+ let closeSelector = this.selectors.input;
808
+ if (!closeSelector && this.selectors.focusable) {
809
+ closeSelector = this.selectors.focusable;
810
+ } else if (!closeSelector) {
811
+ closeSelector = this.selectors.trigger;
812
+ }
813
+ if (closeSelector) {
814
+ const closeElement = page.locator(closeSelector).first();
815
+ await closeElement.focus();
816
+ await page.keyboard.press("Escape");
817
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
818
+ }
819
+ if (!menuClosed && this.selectors.trigger) {
820
+ const triggerElement = page.locator(this.selectors.trigger).first();
821
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
822
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
823
+ }
824
+ if (!menuClosed) {
825
+ await page.mouse.click(10, 10);
826
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
827
+ }
828
+ if (!menuClosed) {
829
+ throw new Error(
830
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
831
+ 1. Escape key
832
+ 2. Clicking trigger
833
+ 3. Clicking outside
834
+ This indicates a problem with the combobox component's close functionality.`
835
+ );
836
+ }
837
+ if (this.selectors.input) {
838
+ await page.locator(this.selectors.input).first().clear();
839
+ }
840
+ }
841
+ async shouldSkipTest() {
842
+ return false;
843
+ }
844
+ getMainSelector() {
845
+ return this.mainSelector;
846
+ }
847
+ };
848
+ }
849
+ });
850
+
851
+ // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
852
+ var AccordionComponentStrategy;
853
+ var init_AccordionComponentStrategy = __esm({
854
+ "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
855
+ "use strict";
856
+ init_test();
857
+ AccordionComponentStrategy = class {
858
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
859
+ this.mainSelector = mainSelector;
860
+ this.selectors = selectors;
861
+ this.actionTimeoutMs = actionTimeoutMs;
862
+ this.assertionTimeoutMs = assertionTimeoutMs;
863
+ }
864
+ async resetState(page) {
865
+ if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
866
+ return;
867
+ }
868
+ const triggerSelector = this.selectors.trigger;
869
+ const panelSelector = this.selectors.panel;
870
+ if (!triggerSelector || !panelSelector) return;
871
+ const allTriggers = await page.locator(triggerSelector).all();
872
+ for (const trigger of allTriggers) {
873
+ const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
874
+ const triggerPanel = await trigger.getAttribute("aria-controls");
875
+ if (isExpanded && triggerPanel) {
876
+ await trigger.click({ timeout: this.actionTimeoutMs });
877
+ const panel = page.locator(`#${triggerPanel}`);
878
+ await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
879
+ });
880
+ }
881
+ }
882
+ }
883
+ async shouldSkipTest() {
884
+ return false;
885
+ }
886
+ getMainSelector() {
887
+ return this.mainSelector;
888
+ }
889
+ };
890
+ }
891
+ });
892
+
893
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
894
+ var MenuComponentStrategy;
895
+ var init_MenuComponentStrategy = __esm({
896
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
897
+ "use strict";
898
+ init_test();
899
+ MenuComponentStrategy = class {
900
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
901
+ this.mainSelector = mainSelector;
902
+ this.selectors = selectors;
903
+ this.actionTimeoutMs = actionTimeoutMs;
904
+ this.assertionTimeoutMs = assertionTimeoutMs;
905
+ }
906
+ async resetState(page) {
907
+ if (!this.selectors.popup) return;
908
+ const popupSelector = this.selectors.popup;
909
+ const popupElement = page.locator(popupSelector).first();
910
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
911
+ if (!isPopupVisible) return;
912
+ let menuClosed = false;
913
+ let closeSelector = this.selectors.input;
914
+ if (!closeSelector && this.selectors.focusable) {
915
+ closeSelector = this.selectors.focusable;
916
+ } else if (!closeSelector) {
917
+ closeSelector = this.selectors.trigger;
918
+ }
919
+ if (closeSelector) {
920
+ const closeElement = page.locator(closeSelector).first();
921
+ await closeElement.focus();
922
+ await page.keyboard.press("Escape");
923
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
924
+ }
925
+ if (!menuClosed && this.selectors.trigger) {
926
+ const triggerElement = page.locator(this.selectors.trigger).first();
927
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
928
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
929
+ }
930
+ if (!menuClosed) {
931
+ await page.mouse.click(10, 10);
932
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
933
+ }
934
+ if (!menuClosed) {
935
+ throw new Error(
936
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
937
+ 1. Escape key
938
+ 2. Clicking trigger
939
+ 3. Clicking outside
940
+ This indicates a problem with the menu component's close functionality.`
941
+ );
942
+ }
943
+ if (this.selectors.input) {
944
+ await page.locator(this.selectors.input).first().clear();
945
+ }
946
+ if (this.selectors.trigger) {
947
+ const triggerElement = page.locator(this.selectors.trigger).first();
948
+ await triggerElement.focus();
949
+ }
950
+ }
951
+ async shouldSkipTest(test, page) {
952
+ for (const act of test.action) {
953
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
954
+ const submenuSelector = this.selectors[act.target];
955
+ if (submenuSelector) {
956
+ const submenuCount = await page.locator(submenuSelector).count();
957
+ if (submenuCount === 0) {
958
+ return true;
959
+ }
960
+ }
961
+ }
962
+ }
963
+ for (const assertion of test.assertions) {
964
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
965
+ const submenuSelector = this.selectors[assertion.target];
966
+ if (submenuSelector) {
967
+ const submenuCount = await page.locator(submenuSelector).count();
968
+ if (submenuCount === 0) {
969
+ return true;
970
+ }
971
+ }
972
+ }
973
+ }
974
+ return false;
975
+ }
976
+ getMainSelector() {
977
+ return this.mainSelector;
978
+ }
979
+ };
980
+ }
981
+ });
982
+
983
+ // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
984
+ var TabsComponentStrategy;
985
+ var init_TabsComponentStrategy = __esm({
986
+ "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
987
+ "use strict";
988
+ TabsComponentStrategy = class {
989
+ constructor(mainSelector, selectors) {
990
+ this.mainSelector = mainSelector;
991
+ this.selectors = selectors;
992
+ }
993
+ async resetState() {
994
+ }
995
+ async shouldSkipTest(test, page) {
996
+ if (test.isVertical !== void 0 && this.selectors.tablist) {
997
+ const tablistSelector = this.selectors.tablist;
998
+ const tablist = page.locator(tablistSelector).first();
999
+ const orientation = await tablist.getAttribute("aria-orientation");
1000
+ const isVertical = orientation === "vertical";
1001
+ if (test.isVertical !== isVertical) {
1002
+ return true;
1003
+ }
1004
+ }
1005
+ return false;
1006
+ }
1007
+ getMainSelector() {
1008
+ return this.mainSelector;
1009
+ }
1010
+ };
1011
+ }
1012
+ });
1013
+
1014
+ // src/utils/test/src/ComponentDetector.ts
1015
+ var import_fs, import_meta2, ComponentDetector;
1016
+ var init_ComponentDetector = __esm({
1017
+ "src/utils/test/src/ComponentDetector.ts"() {
1018
+ "use strict";
1019
+ init_ComboboxComponentStrategy();
1020
+ init_AccordionComponentStrategy();
1021
+ init_MenuComponentStrategy();
1022
+ init_TabsComponentStrategy();
1023
+ import_fs = require("fs");
1024
+ init_contract();
1025
+ import_meta2 = {};
1026
+ ComponentDetector = class {
1027
+ static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
1028
+ const contractTyped = contract_default;
1029
+ const contractPath = contractTyped[componentName]?.path;
1030
+ if (!contractPath) {
1031
+ throw new Error(`Contract path not found for component: ${componentName}`);
1032
+ }
1033
+ const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
1034
+ const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
1035
+ const componentContract = JSON.parse(contractData);
1036
+ const selectors = componentContract.selectors;
1037
+ if (componentName === "combobox") {
1038
+ const mainSelector = selectors.input || selectors.container;
1039
+ return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1040
+ }
1041
+ if (componentName === "accordion") {
1042
+ const mainSelector = selectors.trigger || selectors.container;
1043
+ return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1044
+ }
1045
+ if (componentName === "menu") {
1046
+ const mainSelector = selectors.trigger || selectors.container;
1047
+ return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1048
+ }
1049
+ if (componentName === "tabs") {
1050
+ const mainSelector = selectors.tablist || selectors.tab;
1051
+ return new TabsComponentStrategy(mainSelector, selectors);
1052
+ }
1053
+ return null;
1054
+ }
1055
+ };
1056
+ }
1057
+ });
1058
+
1059
+ // src/utils/test/src/RelativeTargetResolver.ts
1060
+ var RelativeTargetResolver;
1061
+ var init_RelativeTargetResolver = __esm({
1062
+ "src/utils/test/src/RelativeTargetResolver.ts"() {
1063
+ "use strict";
1064
+ RelativeTargetResolver = class {
1065
+ /**
1066
+ * Resolve a relative target like "first", "second", "last", "next", "previous"
1067
+ * @param page Playwright page instance
1068
+ * @param selector Base selector to find elements
1069
+ * @param relative Relative position (first, second, last, next, previous)
1070
+ * @returns The resolved Locator or null if not found
1071
+ */
1072
+ static async resolve(page, selector, relative) {
1073
+ const items = await page.locator(selector).all();
1074
+ switch (relative) {
1075
+ case "first":
1076
+ return items[0];
1077
+ case "second":
1078
+ return items[1];
1079
+ case "last":
1080
+ return items[items.length - 1];
1081
+ case "next": {
1082
+ const currentIndex = await page.evaluate(([sel]) => {
1083
+ const items2 = Array.from(document.querySelectorAll(sel));
1084
+ return items2.indexOf(document.activeElement);
1085
+ }, [selector]);
1086
+ const nextIndex = (currentIndex + 1) % items.length;
1087
+ return items[nextIndex];
1088
+ }
1089
+ case "previous": {
1090
+ const currentIndex = await page.evaluate(([sel]) => {
1091
+ const items2 = Array.from(document.querySelectorAll(sel));
1092
+ return items2.indexOf(document.activeElement);
1093
+ }, [selector]);
1094
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
1095
+ return items[prevIndex];
1096
+ }
1097
+ default:
1098
+ return null;
1099
+ }
1100
+ }
1101
+ };
1102
+ }
1103
+ });
1104
+
1105
+ // src/utils/test/src/ActionExecutor.ts
1106
+ var ActionExecutor;
1107
+ var init_ActionExecutor = __esm({
1108
+ "src/utils/test/src/ActionExecutor.ts"() {
1109
+ "use strict";
1110
+ init_RelativeTargetResolver();
1111
+ ActionExecutor = class {
1112
+ constructor(page, selectors, timeoutMs = 400) {
1113
+ this.page = page;
1114
+ this.selectors = selectors;
1115
+ this.timeoutMs = timeoutMs;
1116
+ }
1117
+ /**
1118
+ * Check if error is due to browser/page being closed
1119
+ */
1120
+ isBrowserClosedError(error) {
1121
+ return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
1122
+ }
1123
+ /**
1124
+ * Execute focus action
1125
+ */
1126
+ async focus(target) {
1127
+ try {
1128
+ const selector = this.selectors[target];
1129
+ if (!selector) {
1130
+ return { success: false, error: `Selector for focus target ${target} not found.` };
1131
+ }
1132
+ await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
1133
+ return { success: true };
1134
+ } catch (error) {
1135
+ if (this.isBrowserClosedError(error)) {
1136
+ return {
1137
+ success: false,
1138
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
1139
+ shouldBreak: true
1140
+ };
1141
+ }
1142
+ return {
1143
+ success: false,
1144
+ error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
1145
+ };
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Execute type/fill action
1150
+ */
1151
+ async type(target, value) {
1152
+ try {
1153
+ const selector = this.selectors[target];
1154
+ if (!selector) {
1155
+ return { success: false, error: `Selector for type target ${target} not found.` };
1156
+ }
1157
+ await this.page.locator(selector).first().fill(value, { timeout: this.timeoutMs });
1158
+ return { success: true };
1159
+ } catch (error) {
1160
+ if (this.isBrowserClosedError(error)) {
1161
+ return {
1162
+ success: false,
1163
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
1164
+ shouldBreak: true
1165
+ };
1166
+ }
1167
+ return {
1168
+ success: false,
1169
+ error: `Failed to type into ${target}: ${error instanceof Error ? error.message : String(error)}`
1170
+ };
1171
+ }
1172
+ }
1173
+ /**
1174
+ * Execute click action
1175
+ */
1176
+ async click(target, relativeTarget) {
1177
+ try {
1178
+ if (target === "document") {
1179
+ await this.page.mouse.click(10, 10);
1180
+ return { success: true };
1181
+ }
1182
+ if (target === "relative" && relativeTarget) {
1183
+ const relativeSelector = this.selectors.relative;
1184
+ if (!relativeSelector) {
1185
+ return { success: false, error: `Relative selector not defined for click action.` };
1186
+ }
1187
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
1188
+ if (!element) {
1189
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
1190
+ }
1191
+ await element.click({ timeout: this.timeoutMs });
1192
+ return { success: true };
1193
+ }
1194
+ const selector = this.selectors[target];
1195
+ if (!selector) {
1196
+ return { success: false, error: `Selector for action target ${target} not found.` };
1197
+ }
1198
+ await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
1199
+ return { success: true };
1200
+ } catch (error) {
1201
+ if (this.isBrowserClosedError(error)) {
1202
+ return {
1203
+ success: false,
1204
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
1205
+ shouldBreak: true
1206
+ };
1207
+ }
1208
+ return {
1209
+ success: false,
1210
+ error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
1211
+ };
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Execute keypress action
1216
+ */
1217
+ async keypress(target, key) {
1218
+ try {
1219
+ const keyMap = {
1220
+ "Space": "Space",
1221
+ "Enter": "Enter",
1222
+ "Escape": "Escape",
1223
+ "Arrow Up": "ArrowUp",
1224
+ "Arrow Down": "ArrowDown",
1225
+ "Arrow Left": "ArrowLeft",
1226
+ "Arrow Right": "ArrowRight",
1227
+ "Home": "Home",
1228
+ "End": "End",
1229
+ "Tab": "Tab"
1230
+ };
1231
+ let keyValue = keyMap[key] || key;
1232
+ if (keyValue === "Space") {
1233
+ keyValue = " ";
1234
+ } else if (keyValue.includes(" ")) {
1235
+ keyValue = keyValue.replace(/ /g, "");
1236
+ }
1237
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
1238
+ await this.page.keyboard.press(keyValue);
1239
+ return { success: true };
1240
+ }
1241
+ const selector = this.selectors[target];
1242
+ if (!selector) {
1243
+ return { success: false, error: `Selector for keypress target ${target} not found.` };
1244
+ }
1245
+ const locator = this.page.locator(selector).first();
1246
+ const elementCount = await locator.count();
1247
+ if (elementCount === 0) {
1248
+ return {
1249
+ success: false,
1250
+ error: `${target} element not found (optional submenu test)`,
1251
+ shouldBreak: true
1252
+ // Signal to skip this test
1253
+ };
1254
+ }
1255
+ await locator.press(keyValue, { timeout: this.timeoutMs });
1256
+ return { success: true };
1257
+ } catch (error) {
1258
+ if (this.isBrowserClosedError(error)) {
1259
+ return {
1260
+ success: false,
1261
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
1262
+ shouldBreak: true
1263
+ };
1264
+ }
1265
+ return {
1266
+ success: false,
1267
+ error: `Failed to press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
1268
+ };
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Execute hover action
1273
+ */
1274
+ async hover(target, relativeTarget) {
1275
+ try {
1276
+ if (target === "relative" && relativeTarget) {
1277
+ const relativeSelector = this.selectors.relative;
1278
+ if (!relativeSelector) {
1279
+ return { success: false, error: `Relative selector not defined for hover action.` };
1280
+ }
1281
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
1282
+ if (!element) {
1283
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
1284
+ }
1285
+ await element.hover({ timeout: this.timeoutMs });
1286
+ return { success: true };
1287
+ }
1288
+ const selector = this.selectors[target];
1289
+ if (!selector) {
1290
+ return { success: false, error: `Selector for hover target ${target} not found.` };
1291
+ }
1292
+ await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
1293
+ return { success: true };
1294
+ } catch (error) {
1295
+ if (this.isBrowserClosedError(error)) {
1296
+ return {
1297
+ success: false,
1298
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
1299
+ shouldBreak: true
1300
+ };
1301
+ }
1302
+ return {
1303
+ success: false,
1304
+ error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
1305
+ };
1306
+ }
1307
+ }
1308
+ };
1309
+ }
1310
+ });
1311
+
1312
+ // src/utils/test/src/AssertionRunner.ts
1313
+ var AssertionRunner;
1314
+ var init_AssertionRunner = __esm({
1315
+ "src/utils/test/src/AssertionRunner.ts"() {
1316
+ "use strict";
1317
+ init_test();
1318
+ init_RelativeTargetResolver();
1319
+ AssertionRunner = class {
1320
+ constructor(page, selectors, timeoutMs = 400) {
1321
+ this.page = page;
1322
+ this.selectors = selectors;
1323
+ this.timeoutMs = timeoutMs;
1324
+ }
1325
+ /**
1326
+ * Resolve the target element for an assertion
1327
+ */
1328
+ async resolveTarget(targetName, relativeTarget) {
1329
+ try {
1330
+ if (targetName === "relative") {
1331
+ const relativeSelector = this.selectors.relative;
1332
+ if (!relativeSelector) {
1333
+ return { target: null, error: "Relative selector is not defined in the contract." };
1334
+ }
1335
+ if (!relativeTarget) {
1336
+ return { target: null, error: "Relative target or expected value is not defined." };
1337
+ }
1338
+ const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
1339
+ if (!target) {
1340
+ return { target: null, error: `Target ${targetName} not found.` };
1341
+ }
1342
+ return { target };
1343
+ }
1344
+ const selector = this.selectors[targetName];
1345
+ if (!selector) {
1346
+ return { target: null, error: `Selector for assertion target ${targetName} not found.` };
1347
+ }
1348
+ return { target: this.page.locator(selector).first() };
1349
+ } catch (error) {
1350
+ return {
1351
+ target: null,
1352
+ error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
1353
+ };
1354
+ }
1355
+ }
1356
+ /**
1357
+ * Validate visibility assertion
1358
+ */
1359
+ async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
1360
+ try {
1361
+ if (expectedVisible) {
1362
+ await (0, test_exports.expect)(target).toBeVisible({ timeout: this.timeoutMs });
1363
+ return {
1364
+ success: true,
1365
+ passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
1366
+ };
1367
+ } else {
1368
+ await (0, test_exports.expect)(target).toBeHidden({ timeout: this.timeoutMs });
1369
+ return {
1370
+ success: true,
1371
+ passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
1372
+ };
1373
+ }
1374
+ } catch {
1375
+ const selector = this.selectors[targetName] || "";
1376
+ const debugState = await this.page.evaluate((sel) => {
1377
+ const el = sel ? document.querySelector(sel) : null;
1378
+ if (!el) return "element not found";
1379
+ const styles = window.getComputedStyle(el);
1380
+ return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
1381
+ }, selector);
1382
+ if (expectedVisible) {
1383
+ return {
1384
+ success: false,
1385
+ failMessage: `${failureMessage} (actual: ${debugState})`
1386
+ };
1387
+ } else {
1388
+ return {
1389
+ success: false,
1390
+ failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
1391
+ };
1392
+ }
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Validate attribute assertion
1397
+ */
1398
+ async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
1399
+ if (expectedValue === "!empty") {
1400
+ const attributeValue2 = await target.getAttribute(attribute);
1401
+ if (attributeValue2 && attributeValue2.trim() !== "") {
1402
+ return {
1403
+ success: true,
1404
+ passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
1405
+ };
1406
+ } else {
1407
+ return {
1408
+ success: false,
1409
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
1410
+ };
1411
+ }
1412
+ }
1413
+ const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
1414
+ const attributeValue = await target.getAttribute(attribute);
1415
+ if (attributeValue !== null && expectedValues.includes(attributeValue)) {
1416
+ return {
1417
+ success: true,
1418
+ passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
1419
+ };
1420
+ } else {
1421
+ return {
1422
+ success: false,
1423
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
1424
+ };
1425
+ }
1426
+ }
1427
+ /**
1428
+ * Validate input value assertion
1429
+ */
1430
+ async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
1431
+ const inputValue = await target.inputValue().catch(() => "");
1432
+ if (expectedValue === "!empty") {
1433
+ if (inputValue && inputValue.trim() !== "") {
1434
+ return {
1435
+ success: true,
1436
+ passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
1437
+ };
1438
+ } else {
1439
+ return {
1440
+ success: false,
1441
+ failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
1442
+ };
1443
+ }
1444
+ }
1445
+ if (expectedValue === "") {
1446
+ if (inputValue === "") {
1447
+ return {
1448
+ success: true,
1449
+ passMessage: `${targetName} has empty value. Test: "${testDescription}".`
1450
+ };
1451
+ } else {
1452
+ return {
1453
+ success: false,
1454
+ failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
1455
+ };
1456
+ }
1457
+ }
1458
+ if (inputValue === expectedValue) {
1459
+ return {
1460
+ success: true,
1461
+ passMessage: `${targetName} has expected value. Test: "${testDescription}".`
1462
+ };
1463
+ } else {
1464
+ return {
1465
+ success: false,
1466
+ failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
1467
+ };
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Validate focus assertion
1472
+ */
1473
+ async validateFocus(target, targetName, failureMessage, testDescription) {
1474
+ try {
1475
+ await (0, test_exports.expect)(target).toBeFocused({ timeout: this.timeoutMs });
1476
+ return {
1477
+ success: true,
1478
+ passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
1479
+ };
1480
+ } catch {
1481
+ const actualFocus = await this.page.evaluate(() => {
1482
+ const focused = document.activeElement;
1483
+ return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
1484
+ });
1485
+ return {
1486
+ success: false,
1487
+ failMessage: `${failureMessage} (actual focus: ${actualFocus})`
1488
+ };
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Validate role assertion
1493
+ */
1494
+ async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
1495
+ const roleValue = await target.getAttribute("role");
1496
+ if (roleValue === expectedRole) {
1497
+ return {
1498
+ success: true,
1499
+ passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
1500
+ };
1501
+ } else {
1502
+ return {
1503
+ success: false,
1504
+ failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
1505
+ };
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Main validation method - routes to specific validators
1510
+ */
1511
+ async validate(assertion, testDescription) {
1512
+ if (this.page.isClosed()) {
1513
+ return {
1514
+ success: false,
1515
+ failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
1516
+ };
1517
+ }
1518
+ const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
1519
+ if (error || !target) {
1520
+ return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
1521
+ }
1522
+ switch (assertion.assertion) {
1523
+ case "toBeVisible":
1524
+ return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
1525
+ case "notToBeVisible":
1526
+ return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
1527
+ case "toHaveAttribute":
1528
+ if (assertion.attribute && assertion.expectedValue !== void 0) {
1529
+ return this.validateAttribute(
1530
+ target,
1531
+ assertion.target,
1532
+ assertion.attribute,
1533
+ assertion.expectedValue,
1534
+ assertion.failureMessage || "",
1535
+ testDescription
1536
+ );
1537
+ }
1538
+ return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
1539
+ case "toHaveValue":
1540
+ if (assertion.expectedValue !== void 0) {
1541
+ return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1542
+ }
1543
+ return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
1544
+ case "toHaveFocus":
1545
+ return this.validateFocus(target, assertion.target, assertion.failureMessage || "", testDescription);
1546
+ case "toHaveRole":
1547
+ if (assertion.expectedValue !== void 0) {
1548
+ return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1549
+ }
1550
+ return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
1551
+ default:
1552
+ return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
1553
+ }
1554
+ }
1555
+ };
1556
+ }
1557
+ });
1558
+
787
1559
  // src/utils/test/contract/contractTestRunnerPlaywright.ts
788
1560
  var contractTestRunnerPlaywright_exports = {};
789
1561
  __export(contractTestRunnerPlaywright_exports, {
@@ -793,20 +1565,13 @@ async function runContractTestsPlaywright(componentName, url) {
793
1565
  const reporter = new ContractReporter(true);
794
1566
  const actionTimeoutMs = 400;
795
1567
  const assertionTimeoutMs = 400;
796
- function isBrowserClosedError(error) {
797
- return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
798
- }
799
1568
  const contractTyped = contract_default;
800
1569
  const contractPath = contractTyped[componentName]?.path;
801
- if (!contractPath) {
802
- throw new Error(`Contract path not found for component: ${componentName}`);
803
- }
804
- const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
805
- const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
1570
+ const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
1571
+ const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
806
1572
  const componentContract = JSON.parse(contractData);
807
1573
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
808
1574
  const apgUrl = componentContract.meta?.source?.apg;
809
- reporter.start(componentName, totalTests, apgUrl);
810
1575
  const failures = [];
811
1576
  const passes = [];
812
1577
  const skipped = [];
@@ -826,17 +1591,28 @@ async function runContractTestsPlaywright(componentName, url) {
826
1591
  }
827
1592
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
828
1593
  }
829
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
1594
+ const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1595
+ if (!strategy) {
1596
+ throw new Error(`Unsupported component: ${componentName}`);
1597
+ }
1598
+ const mainSelector = strategy.getMainSelector();
830
1599
  if (!mainSelector) {
831
- throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
1600
+ throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
832
1601
  }
833
1602
  try {
834
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
1603
+ await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 28e3 });
835
1604
  } catch (error) {
836
1605
  throw new Error(
837
- `CRITICAL: Component element '${mainSelector}' not found on page within 30s. This usually means the component didn't render or the contract selector is incorrect. Original error: ${error instanceof Error ? error.message : String(error)}`
1606
+ `
1607
+ \u274C CRITICAL: Component not found on page!
1608
+ This usually means:
1609
+ - The component didn't render
1610
+ - The URL is incorrect
1611
+ - The component selector '${mainSelector}' in the contract is wrong
1612
+ - Original error: ${error}`
838
1613
  );
839
1614
  }
1615
+ reporter.start(componentName, totalTests, apgUrl);
840
1616
  if (componentName === "menu" && componentContract.selectors.trigger) {
841
1617
  await page.locator(componentContract.selectors.trigger).first().waitFor({
842
1618
  state: "visible",
@@ -845,38 +1621,7 @@ async function runContractTestsPlaywright(componentName, url) {
845
1621
  console.warn("Menu trigger not visible, continuing with tests...");
846
1622
  });
847
1623
  }
848
- async function resolveRelativeTarget(selector, relative) {
849
- if (!page) {
850
- throw new Error("Page is not initialized");
851
- }
852
- const items = await page.locator(selector).all();
853
- switch (relative) {
854
- case "first":
855
- return items[0];
856
- case "second":
857
- return items[1];
858
- case "last":
859
- return items[items.length - 1];
860
- case "next": {
861
- const currentIndex = await page.evaluate(([sel]) => {
862
- const items2 = Array.from(document.querySelectorAll(sel));
863
- return items2.indexOf(document.activeElement);
864
- }, [selector]);
865
- const nextIndex = (currentIndex + 1) % items.length;
866
- return items[nextIndex];
867
- }
868
- case "previous": {
869
- const currentIndex = await page.evaluate(([sel]) => {
870
- const items2 = Array.from(document.querySelectorAll(sel));
871
- return items2.indexOf(document.activeElement);
872
- }, [selector]);
873
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
874
- return items[prevIndex];
875
- }
876
- default:
877
- return null;
878
- }
879
- }
1624
+ const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
880
1625
  for (const test of componentContract.static[0]?.assertions || []) {
881
1626
  if (test.target === "relative") continue;
882
1627
  const targetSelector = componentContract.selectors[test.target];
@@ -929,12 +1674,18 @@ async function runContractTestsPlaywright(componentName, url) {
929
1674
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
930
1675
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
931
1676
  } else {
932
- const attributeValue = await target.getAttribute(test.attribute);
933
- const expectedValues = test.expectedValue.split(" | ");
934
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
935
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
936
- } else {
937
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
1677
+ const result = await staticAssertionRunner.validateAttribute(
1678
+ target,
1679
+ test.target,
1680
+ test.attribute,
1681
+ test.expectedValue,
1682
+ test.failureMessage,
1683
+ "Static ARIA Test"
1684
+ );
1685
+ if (result.success && result.passMessage) {
1686
+ passes.push(result.passMessage);
1687
+ } else if (!result.success && result.failMessage) {
1688
+ failures.push(result.failMessage);
938
1689
  }
939
1690
  }
940
1691
  }
@@ -949,383 +1700,58 @@ async function runContractTestsPlaywright(componentName, url) {
949
1700
  }
950
1701
  const { action, assertions } = dynamicTest;
951
1702
  const failuresBeforeTest = failures.length;
952
- if (componentContract.selectors.popup) {
953
- const popupSelector = componentContract.selectors.popup;
954
- if (!popupSelector) continue;
955
- const popupElement = page.locator(popupSelector).first();
956
- const isPopupVisible = await popupElement.isVisible().catch(() => false);
957
- if (isPopupVisible) {
958
- let menuClosed = false;
959
- let closeSelector = componentContract.selectors.input;
960
- if (!closeSelector && componentContract.selectors.focusable) {
961
- closeSelector = componentContract.selectors.focusable;
962
- } else if (!closeSelector) {
963
- closeSelector = componentContract.selectors.trigger;
964
- }
965
- if (closeSelector) {
966
- const closeElement = page.locator(closeSelector).first();
967
- await closeElement.focus();
968
- await page.keyboard.press("Escape");
969
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
970
- }
971
- if (!menuClosed && componentContract.selectors.trigger) {
972
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
973
- await triggerElement.click({ timeout: actionTimeoutMs });
974
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
975
- }
976
- if (!menuClosed) {
977
- await page.mouse.click(10, 10);
978
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
979
- }
980
- if (!menuClosed) {
981
- throw new Error(
982
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
983
- 1. Escape key
984
- 2. Clicking trigger
985
- 3. Clicking outside
986
- This indicates a problem with the menu component's close functionality.`
987
- );
988
- }
989
- if (componentContract.selectors.input) {
990
- await page.locator(componentContract.selectors.input).first().clear();
991
- }
992
- if (componentName === "menu" && componentContract.selectors.trigger) {
993
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
994
- await triggerElement.focus();
995
- }
996
- }
997
- }
998
- if (componentContract.selectors.panel && componentContract.selectors.trigger && !componentContract.selectors.popup) {
999
- const triggerSelector = componentContract.selectors.trigger;
1000
- const panelSelector = componentContract.selectors.panel;
1001
- if (triggerSelector && panelSelector) {
1002
- const allTriggers = await page.locator(triggerSelector).all();
1003
- for (const trigger of allTriggers) {
1004
- const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
1005
- const triggerPanel = await trigger.getAttribute("aria-controls");
1006
- if (isExpanded && triggerPanel) {
1007
- await trigger.click({ timeout: actionTimeoutMs });
1008
- const panel = page.locator(`#${triggerPanel}`);
1009
- await (0, test_exports.expect)(panel).toBeHidden({ timeout: assertionTimeoutMs }).catch(() => {
1010
- });
1011
- }
1012
- }
1013
- }
1014
- }
1015
- let shouldSkipTest = false;
1016
- for (const act of action) {
1017
- if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
1018
- const submenuSelector = componentContract.selectors[act.target];
1019
- if (submenuSelector) {
1020
- const submenuCount = await page.locator(submenuSelector).count();
1021
- if (submenuCount === 0) {
1022
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
1023
- shouldSkipTest = true;
1024
- break;
1025
- }
1026
- }
1027
- }
1028
- }
1029
- if (!shouldSkipTest) {
1030
- for (const assertion of assertions) {
1031
- if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
1032
- const submenuSelector = componentContract.selectors[assertion.target];
1033
- if (submenuSelector) {
1034
- const submenuCount = await page.locator(submenuSelector).count();
1035
- if (submenuCount === 0) {
1036
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
1037
- shouldSkipTest = true;
1038
- break;
1039
- }
1040
- }
1041
- }
1042
- }
1703
+ try {
1704
+ await strategy.resetState(page);
1705
+ } catch (error) {
1706
+ const errorMessage = error instanceof Error ? error.message : String(error);
1707
+ reporter.error(errorMessage);
1708
+ throw error;
1043
1709
  }
1710
+ const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1044
1711
  if (shouldSkipTest) {
1712
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
1045
1713
  continue;
1046
1714
  }
1047
- if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
1048
- if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
1049
- const tablistSelector = componentContract.selectors.tablist;
1050
- const tablist = page.locator(tablistSelector).first();
1051
- const orientation = await tablist.getAttribute("aria-orientation");
1052
- const isVertical = orientation === "vertical";
1053
- if (dynamicTest.isVertical !== isVertical) {
1054
- const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
1055
- reporter.reportTest(dynamicTest, "skip", skipReason);
1056
- continue;
1057
- }
1058
- }
1059
- }
1715
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1716
+ const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1060
1717
  for (const act of action) {
1061
1718
  if (!page || page.isClosed()) {
1062
1719
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1063
1720
  break;
1064
1721
  }
1722
+ let result;
1065
1723
  if (act.type === "focus") {
1066
- try {
1067
- const focusSelector = componentContract.selectors[act.target];
1068
- if (!focusSelector) {
1069
- failures.push(`Selector for focus target ${act.target} not found.`);
1070
- continue;
1071
- }
1072
- await page.locator(focusSelector).first().focus({ timeout: actionTimeoutMs });
1073
- } catch (error) {
1074
- if (isBrowserClosedError(error)) {
1075
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1076
- break;
1077
- }
1078
- failures.push(`Failed to focus ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
1079
- continue;
1080
- }
1081
- }
1082
- if (act.type === "type" && act.value) {
1083
- try {
1084
- const typeSelector = componentContract.selectors[act.target];
1085
- if (!typeSelector) {
1086
- failures.push(`Selector for type target ${act.target} not found.`);
1087
- continue;
1088
- }
1089
- await page.locator(typeSelector).first().fill(act.value, { timeout: actionTimeoutMs });
1090
- } catch (error) {
1091
- if (isBrowserClosedError(error)) {
1092
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1093
- break;
1094
- }
1095
- failures.push(`Failed to type into ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
1096
- continue;
1097
- }
1098
- }
1099
- if (act.type === "click") {
1100
- try {
1101
- if (act.target === "document") {
1102
- await page.mouse.click(10, 10);
1103
- } else if (act.target === "relative" && act.relativeTarget) {
1104
- const relativeSelector = componentContract.selectors.relative;
1105
- if (!relativeSelector) {
1106
- failures.push(`Relative selector not defined for click action.`);
1107
- continue;
1108
- }
1109
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
1110
- if (!relativeElement) {
1111
- failures.push(`Could not resolve relative target ${act.relativeTarget} for click.`);
1112
- continue;
1113
- }
1114
- await relativeElement.click({ timeout: actionTimeoutMs });
1115
- } else {
1116
- const actionSelector = componentContract.selectors[act.target];
1117
- if (!actionSelector) {
1118
- failures.push(`Selector for action target ${act.target} not found.`);
1119
- continue;
1120
- }
1121
- await page.locator(actionSelector).first().click({ timeout: actionTimeoutMs });
1122
- }
1123
- } catch (error) {
1124
- if (isBrowserClosedError(error)) {
1125
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1126
- break;
1127
- }
1128
- failures.push(`Failed to click ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
1129
- continue;
1130
- }
1724
+ result = await actionExecutor.focus(act.target);
1725
+ } else if (act.type === "type" && act.value) {
1726
+ result = await actionExecutor.type(act.target, act.value);
1727
+ } else if (act.type === "click") {
1728
+ result = await actionExecutor.click(act.target, act.relativeTarget);
1729
+ } else if (act.type === "keypress" && act.key) {
1730
+ result = await actionExecutor.keypress(act.target, act.key);
1731
+ } else if (act.type === "hover") {
1732
+ result = await actionExecutor.hover(act.target, act.relativeTarget);
1733
+ } else {
1734
+ continue;
1131
1735
  }
1132
- if (act.type === "keypress" && act.key) {
1133
- try {
1134
- const keyMap = {
1135
- "Space": "Space",
1136
- "Enter": "Enter",
1137
- "Escape": "Escape",
1138
- "Arrow Up": "ArrowUp",
1139
- "Arrow Down": "ArrowDown",
1140
- "Arrow Left": "ArrowLeft",
1141
- "Arrow Right": "ArrowRight",
1142
- "Home": "Home",
1143
- "End": "End",
1144
- "Tab": "Tab"
1145
- };
1146
- let keyValue = keyMap[act.key] || act.key;
1147
- if (keyValue === "Space") {
1148
- keyValue = " ";
1149
- } else if (keyValue.includes(" ")) {
1150
- keyValue = keyValue.replace(/ /g, "");
1151
- }
1152
- if (act.target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
1153
- await page.keyboard.press(keyValue);
1154
- } else {
1155
- const keypressSelector = componentContract.selectors[act.target];
1156
- if (!keypressSelector) {
1157
- failures.push(`Selector for keypress target ${act.target} not found.`);
1158
- continue;
1159
- }
1160
- const target = page.locator(keypressSelector).first();
1161
- const elementCount = await target.count();
1162
- if (elementCount === 0) {
1163
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
1164
- break;
1165
- }
1166
- await target.press(keyValue, { timeout: actionTimeoutMs });
1167
- }
1168
- } catch (error) {
1169
- if (isBrowserClosedError(error)) {
1170
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1171
- break;
1172
- }
1173
- failures.push(`Failed to press ${act.key} on ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
1174
- continue;
1736
+ if (!result.success) {
1737
+ if (result.error) {
1738
+ failures.push(result.error);
1175
1739
  }
1176
- }
1177
- if (act.type === "hover") {
1178
- try {
1179
- if (act.target === "relative" && act.relativeTarget) {
1180
- const relativeSelector = componentContract.selectors.relative;
1181
- if (!relativeSelector) {
1182
- failures.push(`Relative selector not defined for hover action.`);
1183
- continue;
1184
- }
1185
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
1186
- if (!relativeElement) {
1187
- failures.push(`Could not resolve relative target ${act.relativeTarget} for hover.`);
1188
- continue;
1189
- }
1190
- await relativeElement.hover({ timeout: actionTimeoutMs });
1191
- } else {
1192
- const hoverSelector = componentContract.selectors[act.target];
1193
- if (!hoverSelector) {
1194
- failures.push(`Selector for hover target ${act.target} not found.`);
1195
- continue;
1196
- }
1197
- await page.locator(hoverSelector).first().hover({ timeout: actionTimeoutMs });
1740
+ if (result.shouldBreak) {
1741
+ if (result.error?.includes("optional submenu test")) {
1742
+ reporter.reportTest(dynamicTest, "skip", result.error);
1198
1743
  }
1199
- } catch (error) {
1200
- if (isBrowserClosedError(error)) {
1201
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1202
- break;
1203
- }
1204
- failures.push(`Failed to hover ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
1205
- continue;
1744
+ break;
1206
1745
  }
1746
+ continue;
1207
1747
  }
1208
1748
  }
1209
1749
  for (const assertion of assertions) {
1210
- if (!page || page.isClosed()) {
1211
- failures.push(`CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`);
1212
- break;
1213
- }
1214
- let target;
1215
- try {
1216
- if (assertion.target === "relative") {
1217
- const relativeSelector = componentContract.selectors.relative;
1218
- if (!relativeSelector) {
1219
- failures.push("Relative selector is not defined in the contract.");
1220
- continue;
1221
- }
1222
- const relativeTargetValue = assertion.relativeTarget || assertion.expectedValue;
1223
- if (!relativeTargetValue) {
1224
- failures.push("Relative target or expected value is not defined.");
1225
- continue;
1226
- }
1227
- target = await resolveRelativeTarget(relativeSelector, relativeTargetValue);
1228
- } else {
1229
- const assertionSelector = componentContract.selectors[assertion.target];
1230
- if (!assertionSelector) {
1231
- failures.push(`Selector for assertion target ${assertion.target} not found.`);
1232
- continue;
1233
- }
1234
- target = page.locator(assertionSelector).first();
1235
- }
1236
- if (!target) {
1237
- failures.push(`Target ${assertion.target} not found.`);
1238
- continue;
1239
- }
1240
- } catch (error) {
1241
- failures.push(`Failed to resolve target ${assertion.target}: ${error instanceof Error ? error.message : String(error)}`);
1242
- continue;
1243
- }
1244
- if (assertion.assertion === "toBeVisible") {
1245
- try {
1246
- await (0, test_exports.expect)(target).toBeVisible({ timeout: assertionTimeoutMs });
1247
- passes.push(`${assertion.target} is visible as expected. Test: "${dynamicTest.description}".`);
1248
- } catch {
1249
- const debugState = await page.evaluate((sel) => {
1250
- const el = sel ? document.querySelector(sel) : null;
1251
- if (!el) return "element not found";
1252
- const styles = window.getComputedStyle(el);
1253
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
1254
- }, componentContract.selectors[assertion.target] || "");
1255
- failures.push(`${assertion.failureMessage} (actual: ${debugState})`);
1256
- }
1257
- }
1258
- if (assertion.assertion === "notToBeVisible") {
1259
- try {
1260
- await (0, test_exports.expect)(target).toBeHidden({ timeout: assertionTimeoutMs });
1261
- passes.push(`${assertion.target} is not visible as expected. Test: "${dynamicTest.description}".`);
1262
- } catch {
1263
- const debugState = await page.evaluate((sel) => {
1264
- const el = sel ? document.querySelector(sel) : null;
1265
- if (!el) return "element not found";
1266
- const styles = window.getComputedStyle(el);
1267
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
1268
- }, componentContract.selectors[assertion.target] || "");
1269
- failures.push(assertion.failureMessage + ` ${assertion.target} is still visible (actual: ${debugState}).`);
1270
- }
1271
- }
1272
- if (assertion.assertion === "toHaveAttribute" && assertion.attribute && assertion.expectedValue) {
1273
- try {
1274
- if (assertion.expectedValue === "!empty") {
1275
- const attributeValue = await target.getAttribute(assertion.attribute);
1276
- if (attributeValue && attributeValue.trim() !== "") {
1277
- passes.push(`${assertion.target} has non-empty "${assertion.attribute}". Test: "${dynamicTest.description}".`);
1278
- } else {
1279
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should not be empty, found "${attributeValue}".`);
1280
- }
1281
- } else {
1282
- await (0, test_exports.expect)(target).toHaveAttribute(assertion.attribute, assertion.expectedValue, { timeout: assertionTimeoutMs });
1283
- passes.push(`${assertion.target} has expected "${assertion.attribute}". Test: "${dynamicTest.description}".`);
1284
- }
1285
- } catch {
1286
- const attributeValue = await target.getAttribute(assertion.attribute);
1287
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should be "${assertion.expectedValue}", found "${attributeValue}".`);
1288
- }
1289
- }
1290
- if (assertion.assertion === "toHaveValue") {
1291
- const inputValue = await target.inputValue().catch(() => "");
1292
- if (assertion.expectedValue === "!empty") {
1293
- if (inputValue && inputValue.trim() !== "") {
1294
- passes.push(`${assertion.target} has non-empty value. Test: "${dynamicTest.description}".`);
1295
- } else {
1296
- failures.push(assertion.failureMessage + ` ${assertion.target} value should not be empty, found "${inputValue}".`);
1297
- }
1298
- } else if (assertion.expectedValue === "") {
1299
- if (inputValue === "") {
1300
- passes.push(`${assertion.target} has empty value. Test: "${dynamicTest.description}".`);
1301
- } else {
1302
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be empty, found "${inputValue}".`);
1303
- }
1304
- } else if (inputValue === assertion.expectedValue) {
1305
- passes.push(`${assertion.target} has expected value. Test: "${dynamicTest.description}".`);
1306
- } else {
1307
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be "${assertion.expectedValue}", found "${inputValue}".`);
1308
- }
1309
- }
1310
- if (assertion.assertion === "toHaveFocus") {
1311
- try {
1312
- await (0, test_exports.expect)(target).toBeFocused({ timeout: assertionTimeoutMs });
1313
- passes.push(`${assertion.target} has focus as expected. Test: "${dynamicTest.description}".`);
1314
- } catch {
1315
- const actualFocus = await page.evaluate(() => {
1316
- const focused = document.activeElement;
1317
- return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
1318
- });
1319
- failures.push(`${assertion.failureMessage} (actual focus: ${actualFocus})`);
1320
- }
1321
- }
1322
- if (assertion.assertion === "toHaveRole" && assertion.expectedValue) {
1323
- const roleValue = await target.getAttribute("role");
1324
- if (roleValue === assertion.expectedValue) {
1325
- passes.push(`${assertion.target} has role "${assertion.expectedValue}". Test: "${dynamicTest.description}".`);
1326
- } else {
1327
- failures.push(assertion.failureMessage + ` Expected role "${assertion.expectedValue}", found "${roleValue}".`);
1328
- }
1750
+ const result = await assertionRunner.validate(assertion, dynamicTest.description);
1751
+ if (result.success && result.passMessage) {
1752
+ passes.push(result.passMessage);
1753
+ } else if (!result.success && result.failMessage) {
1754
+ failures.push(result.failMessage);
1329
1755
  }
1330
1756
  }
1331
1757
  const failuresAfterTest = failures.length;
@@ -1345,47 +1771,21 @@ This indicates a problem with the menu component's close functionality.`
1345
1771
  } catch (error) {
1346
1772
  if (error instanceof Error) {
1347
1773
  if (error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch")) {
1348
- console.error("\n\u274C CRITICAL: Playwright browsers not found!\n");
1349
- console.log("\u{1F4E6} Run: npx playwright install chromium\n");
1350
- failures.push("CRITICAL: Playwright browser not installed. Run: npx playwright install chromium");
1774
+ throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
1351
1775
  } else if (error.message.includes("net::ERR_CONNECTION_REFUSED") || error.message.includes("NS_ERROR_CONNECTION_REFUSED")) {
1352
- console.error("\n\u274C CRITICAL: Cannot connect to dev server!\n");
1353
- console.log(` Make sure your dev server is running at ${url}
1354
- `);
1355
- failures.push(`CRITICAL: Dev server not running at ${url}`);
1776
+ throw new Error(`
1777
+ \u274C CRITICAL: Cannot connect to dev server!
1778
+ Make sure your dev server is running at ${url}`);
1356
1779
  } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
1357
- console.error("\n\u274C CRITICAL: Component not found on page!\n");
1358
- console.log(` The component selector could not be found within 30 seconds.
1359
- `);
1360
- console.log(` This usually means:
1361
- `);
1362
- console.log(` - The component didn't render
1363
- `);
1364
- console.log(` - The URL is incorrect
1365
- `);
1366
- console.log(` - The component selector in the contract is wrong
1367
- `);
1368
- failures.push(`CRITICAL: Component element not found on page - ${error.message}`);
1780
+ throw new Error(
1781
+ "\n\u274C CRITICAL: Component not found on page!\nThe component selector could not be found within 30 seconds.\nThis usually means:\n - The component didn't render\n - The URL is incorrect\n - The component selector was not provided to the component utility, or a wrong selector was used\n"
1782
+ );
1369
1783
  } else if (error.message.includes("Target page, context or browser has been closed")) {
1370
- console.error("\n\u274C CRITICAL: Browser/page was closed unexpectedly!\n");
1371
- console.log(` This usually means:
1372
- `);
1373
- console.log(` - The test timeout was too short
1374
- `);
1375
- console.log(` - The browser crashed
1376
- `);
1377
- console.log(` - An external process killed the browser
1378
- `);
1379
- failures.push(`CRITICAL: Browser/page closed unexpectedly - ${error.message}`);
1380
- } else if (error.message.includes("FATAL")) {
1381
- console.error(`
1382
- ${error.message}
1383
- `);
1384
- failures.push(error.message);
1784
+ throw new Error(
1785
+ "\n\u274C CRITICAL: Browser/page was closed unexpectedly!\nThis usually means:\n - The test timeout was too short\n - The browser crashed\n - An external process killed the browser"
1786
+ );
1385
1787
  } else {
1386
- console.error("\n\u274C UNEXPECTED ERROR:", error.message);
1387
- console.error("Stack:", error.stack);
1388
- failures.push(`UNEXPECTED ERROR: ${error.message}`);
1788
+ throw error;
1389
1789
  }
1390
1790
  }
1391
1791
  } finally {
@@ -1393,16 +1793,18 @@ ${error.message}
1393
1793
  }
1394
1794
  return { passes, failures, skipped };
1395
1795
  }
1396
- var import_fs, import_meta2;
1796
+ var import_fs2, import_meta3;
1397
1797
  var init_contractTestRunnerPlaywright = __esm({
1398
1798
  "src/utils/test/contract/contractTestRunnerPlaywright.ts"() {
1399
1799
  "use strict";
1400
- init_test();
1401
- import_fs = require("fs");
1800
+ import_fs2 = require("fs");
1402
1801
  init_contract();
1403
- init_ContractReporter();
1404
1802
  init_playwrightTestHarness();
1405
- import_meta2 = {};
1803
+ init_ComponentDetector();
1804
+ init_ContractReporter();
1805
+ init_ActionExecutor();
1806
+ init_AssertionRunner();
1807
+ import_meta3 = {};
1406
1808
  }
1407
1809
  });
1408
1810
 
@@ -1515,6 +1917,11 @@ var init_test2 = __esm({
1515
1917
  init_contractTestRunner();
1516
1918
  init_playwrightTestHarness();
1517
1919
  runTest = async () => {
1920
+ return {
1921
+ passes: [],
1922
+ failures: [],
1923
+ skipped: []
1924
+ };
1518
1925
  };
1519
1926
  if (typeof window === "undefined") {
1520
1927
  runTest = async () => {
@@ -1522,36 +1929,36 @@ var init_test2 = __esm({
1522
1929
  `);
1523
1930
  const { exec } = await import("child_process");
1524
1931
  const chalk3 = (await import("chalk")).default;
1525
- exec(
1526
- `npx vitest --run --reporter verbose`,
1527
- { cwd: process.cwd() },
1528
- async (error, stdout, stderr) => {
1529
- if (stdout) {
1932
+ return new Promise((resolve, reject) => {
1933
+ exec(
1934
+ `npx vitest --run --reporter verbose`,
1935
+ async (error, stdout, stderr) => {
1530
1936
  console.log(stdout);
1531
- }
1532
- if (stderr) {
1533
- console.error(stderr);
1534
- }
1535
- if (!error || error.code === 0) {
1536
- try {
1537
- const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
1538
- displayBadgeInfo2("component");
1539
- await promptAddBadge2("component", process.cwd());
1540
- console.log(chalk3.dim("\n" + "\u2500".repeat(60)));
1541
- console.log(chalk3.cyan("\u{1F499} Found aria-ease helpful?"));
1542
- console.log(chalk3.white(" \u2022 Star us on GitHub: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease"));
1543
- console.log(chalk3.white(" \u2022 Share feedback: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
1544
- console.log(chalk3.dim("\u2500".repeat(60) + "\n"));
1545
- } catch (badgeError) {
1546
- console.error("Warning: Could not display badge prompt:", badgeError);
1937
+ if (stderr) console.error(stderr);
1938
+ const testsPassed = !error || error.code === 0;
1939
+ if (testsPassed) {
1940
+ try {
1941
+ const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
1942
+ displayBadgeInfo2("component");
1943
+ await promptAddBadge2("component", process.cwd());
1944
+ console.log(chalk3.dim("\n" + "\u2500".repeat(60)));
1945
+ console.log(chalk3.cyan("\u{1F499} Found aria-ease helpful?"));
1946
+ console.log(chalk3.white(" \u2022 Star us on GitHub: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease"));
1947
+ console.log(chalk3.white(" \u2022 Share feedback: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
1948
+ console.log(chalk3.dim("\u2500".repeat(60) + "\n"));
1949
+ } catch (badgeError) {
1950
+ console.error("Warning: Could not display badge prompt:", badgeError);
1951
+ }
1952
+ resolve({ passes: [], failures: [], skipped: [] });
1953
+ process.exit(0);
1954
+ } else {
1955
+ const exitCode = error?.code || 1;
1956
+ reject(new Error(`Tests failed with code ${exitCode}`));
1957
+ process.exit(exitCode);
1547
1958
  }
1548
- process.exit(0);
1549
- } else {
1550
- const exitCode = error?.code || 1;
1551
- process.exit(exitCode);
1552
1959
  }
1553
- }
1554
- );
1960
+ );
1961
+ });
1555
1962
  };
1556
1963
  }
1557
1964
  }
@@ -1796,8 +2203,6 @@ program.command("audit").description("Run axe-core powered accessibility audit o
1796
2203
  console.log(import_chalk2.default.yellow(` ${totalViolations} violation${totalViolations !== 1 ? "s" : ""} detected across ${allResults.length} page${allResults.length !== 1 ? "s" : ""}.`));
1797
2204
  console.log(import_chalk2.default.gray(` Review the generated report for details.
1798
2205
  `));
1799
- displayBadgeInfo("audit");
1800
- await promptAddBadge("audit", process.cwd());
1801
2206
  console.log(import_chalk2.default.dim("\n" + "\u2500".repeat(60)));
1802
2207
  console.log(import_chalk2.default.cyan("\u{1F499} Found aria-ease helpful?"));
1803
2208
  console.log(import_chalk2.default.white(" \u2022 Star us on GitHub: ") + import_chalk2.default.blue.underline("https://github.com/aria-ease/aria-ease"));