@molecule/app-e2e-fixtures-default 1.0.2 → 1.0.3

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/runtime.js CHANGED
@@ -14,18 +14,24 @@
14
14
  * @module
15
15
  */
16
16
  /** Bumped when the in-page runtime's protocol changes; a page holding an older one is re-installed. */
17
- export const E2E_RUNTIME_VERSION = 1;
17
+ export const E2E_RUNTIME_VERSION = 2;
18
18
  /**
19
19
  * Install the runtime. Runs INSIDE the page — see the module doc; the body
20
20
  * must stay self-contained.
21
+ *
22
+ * The runtime never waits: every call is ONE attempt, and a call that would
23
+ * have to wait (an element not there yet, not visible yet, covered) answers
24
+ * `{ retry: true, error }` so the DRIVER polls with its own clock. A page in a
25
+ * background tab has its timers throttled to once a second (a minute, after a
26
+ * while) — a `setTimeout` in here would stretch every auto-wait by that much,
27
+ * while the WebSocket messages that carry the calls are delivered on time.
21
28
  */
22
29
  export function installE2ERuntime() {
23
- const VERSION = 1;
30
+ const VERSION = 2;
24
31
  const g = globalThis;
25
32
  if (g.__molE2E && g.__molE2E.version === VERSION)
26
33
  return { version: VERSION };
27
34
  const norm = (s) => (s ?? '').replace(/\s+/g, ' ').trim();
28
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29
35
  const matchText = (text, m) => {
30
36
  if (!m)
31
37
  return true;
@@ -811,257 +817,242 @@ export function installE2ERuntime() {
811
817
  'clear',
812
818
  ]);
813
819
  const ACTIONS_NEEDING_POINTER = new Set(['click', 'dblclick', 'hover', 'tap']);
814
- const act = async (steps, action, opts, deadline) => {
820
+ const act = async (steps, action, opts) => {
815
821
  let reason;
816
- for (;;) {
817
- const els = resolveChain(steps);
818
- if (els.length > 1)
819
- return { ok: false, strict: true, count: els.length };
820
- const el = els[0];
821
- if (!el)
822
- reason = describe(steps) + ' element not found';
823
- else if (ACTIONS_NEEDING_VISIBLE.has(action) && !opts.force && !isVisible(el))
824
- reason = describe(steps) + ' element is not visible';
825
- else if (ACTIONS_NEEDING_ENABLED.has(action) && !opts.force && isDisabled(el))
826
- reason = describe(steps) + ' element is disabled';
827
- else if (['fill', 'type', 'clear'].includes(action) && !opts.force && !isEditable(el))
828
- return {
829
- ok: false,
830
- error: describe(steps) +
831
- ' — element is not an <input>, <textarea>, <select> or [contenteditable] element',
832
- };
833
- else {
834
- if (ACTIONS_NEEDING_POINTER.has(action) || action === 'scrollIntoViewIfNeeded') {
835
- if (!opts.noScroll) {
836
- try {
837
- el.scrollIntoView({ block: 'center', inline: 'center' });
838
- }
839
- catch (_error) {
840
- /* ignore */
841
- }
842
- }
843
- if (action === 'scrollIntoViewIfNeeded')
844
- return { ok: true };
845
- const p = pointAt(el, opts.position);
846
- if (!opts.force) {
847
- const hit = typeof document.elementFromPoint === 'function'
848
- ? document.elementFromPoint(p.x, p.y)
849
- : null;
850
- // A null hit with the point inside the viewport is a document that cannot hit-test
851
- // (jsdom, a detached rendering); treat it as uncovered rather than unreachable.
852
- const inView = p.x >= 0 && p.y >= 0 && p.x <= innerWidth && p.y <= innerHeight;
853
- if (!(hit === null && inView) && !hitOk(el, hit)) {
854
- if (!hit || p.x < 0 || p.y < 0 || p.x > innerWidth || p.y > innerHeight)
855
- reason = describe(steps) + ' — element is outside of the viewport';
856
- else {
857
- const h = hit;
858
- reason =
859
- describe(steps) +
860
- ' — <' +
861
- h.tagName.toLowerCase() +
862
- (h.id ? '#' + h.id : '') +
863
- (h.className && typeof h.className === 'string'
864
- ? '.' + h.className.trim().split(/\s+/).slice(0, 2).join('.')
865
- : '') +
866
- '> intercepts pointer events';
867
- }
868
- if (Date.now() > deadline)
869
- return {
870
- ok: false,
871
- error: reason ?? 'waiting for ' + describe(steps),
872
- timeout: true,
873
- };
874
- await sleep(50);
875
- continue;
876
- }
822
+ const els = resolveChain(steps);
823
+ if (els.length > 1)
824
+ return { ok: false, strict: true, count: els.length };
825
+ const el = els[0];
826
+ if (!el)
827
+ reason = describe(steps) + ' — element not found';
828
+ else if (ACTIONS_NEEDING_VISIBLE.has(action) && !opts.force && !isVisible(el))
829
+ reason = describe(steps) + ' element is not visible';
830
+ else if (ACTIONS_NEEDING_ENABLED.has(action) && !opts.force && isDisabled(el))
831
+ reason = describe(steps) + ' element is disabled';
832
+ else if (['fill', 'type', 'clear'].includes(action) && !opts.force && !isEditable(el))
833
+ return {
834
+ ok: false,
835
+ error: describe(steps) +
836
+ ' element is not an <input>, <textarea>, <select> or [contenteditable] element',
837
+ };
838
+ else {
839
+ if (ACTIONS_NEEDING_POINTER.has(action) || action === 'scrollIntoViewIfNeeded') {
840
+ if (!opts.noScroll) {
841
+ try {
842
+ el.scrollIntoView({ block: 'center', inline: 'center' });
877
843
  }
878
- if (action === 'hover')
879
- hoverAt(el, p.x, p.y);
880
- else if (action === 'tap') {
881
- fire(el, 'touchstart');
882
- fire(el, 'touchend');
883
- clickAt(el, p.x, p.y, opts);
844
+ catch (_error) {
845
+ /* ignore */
884
846
  }
885
- else
886
- clickAt(el, p.x, p.y, {
887
- ...opts,
888
- clickCount: action === 'dblclick' ? 2 : (opts.clickCount ?? 1),
889
- });
890
- return { ok: true };
891
847
  }
892
- if (action === 'focus') {
893
- ;
894
- el.focus();
848
+ if (action === 'scrollIntoViewIfNeeded')
895
849
  return { ok: true };
896
- }
897
- if (action === 'blur') {
898
- ;
899
- el.blur();
900
- return { ok: true };
901
- }
902
- if (action === 'fill' || action === 'clear') {
903
- const value = action === 'clear' ? '' : String(opts.value ?? '');
904
- el.focus();
905
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
906
- const simple = !(el instanceof HTMLInputElement) ||
907
- !['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'image'].includes(el.type);
908
- if (!simple)
909
- return {
910
- ok: false,
911
- error: describe(steps) + ' — cannot fill an input of type ' + el.type,
912
- };
913
- if (el instanceof HTMLInputElement &&
914
- [
915
- 'date',
916
- 'time',
917
- 'datetime-local',
918
- 'month',
919
- 'week',
920
- 'color',
921
- 'range',
922
- 'number',
923
- ].includes(el.type)) {
924
- nativeSetValue(el, value);
925
- fire(el, 'input', { inputType: 'insertText', data: value });
926
- }
850
+ const p = pointAt(el, opts.position);
851
+ if (!opts.force) {
852
+ const hit = typeof document.elementFromPoint === 'function'
853
+ ? document.elementFromPoint(p.x, p.y)
854
+ : null;
855
+ // A null hit with the point inside the viewport is a document that cannot hit-test
856
+ // (jsdom, a detached rendering); treat it as uncovered rather than unreachable.
857
+ const inView = p.x >= 0 && p.y >= 0 && p.x <= innerWidth && p.y <= innerHeight;
858
+ if (!(hit === null && inView) && !hitOk(el, hit)) {
859
+ if (!hit || p.x < 0 || p.y < 0 || p.x > innerWidth || p.y > innerHeight)
860
+ reason = describe(steps) + ' — element is outside of the viewport';
927
861
  else {
928
- try {
929
- el.select();
930
- }
931
- catch (_error) {
932
- /* ignore */
933
- }
934
- fire(el, 'beforeinput', { inputType: 'insertText', data: value });
935
- nativeSetValue(el, value);
936
- try {
937
- el.setSelectionRange(value.length, value.length);
938
- }
939
- catch (_error) {
940
- /* ignore */
941
- }
942
- fire(el, 'input', { inputType: 'insertText', data: value });
862
+ const h = hit;
863
+ reason =
864
+ describe(steps) +
865
+ ' <' +
866
+ h.tagName.toLowerCase() +
867
+ (h.id ? '#' + h.id : '') +
868
+ (h.className && typeof h.className === 'string'
869
+ ? '.' + h.className.trim().split(/\s+/).slice(0, 2).join('.')
870
+ : '') +
871
+ '> intercepts pointer events';
943
872
  }
944
- fire(el, 'change');
873
+ return { ok: false, error: reason, retry: true };
945
874
  }
946
- else if (el instanceof HTMLSelectElement) {
875
+ }
876
+ if (action === 'hover')
877
+ hoverAt(el, p.x, p.y);
878
+ else if (action === 'tap') {
879
+ fire(el, 'touchstart');
880
+ fire(el, 'touchend');
881
+ clickAt(el, p.x, p.y, opts);
882
+ }
883
+ else
884
+ clickAt(el, p.x, p.y, {
885
+ ...opts,
886
+ clickCount: action === 'dblclick' ? 2 : (opts.clickCount ?? 1),
887
+ });
888
+ return { ok: true };
889
+ }
890
+ if (action === 'focus') {
891
+ ;
892
+ el.focus();
893
+ return { ok: true };
894
+ }
895
+ if (action === 'blur') {
896
+ ;
897
+ el.blur();
898
+ return { ok: true };
899
+ }
900
+ if (action === 'fill' || action === 'clear') {
901
+ const value = action === 'clear' ? '' : String(opts.value ?? '');
902
+ el.focus();
903
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
904
+ const simple = !(el instanceof HTMLInputElement) ||
905
+ !['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'image'].includes(el.type);
906
+ if (!simple)
907
+ return {
908
+ ok: false,
909
+ error: describe(steps) + ' — cannot fill an input of type ' + el.type,
910
+ };
911
+ if (el instanceof HTMLInputElement &&
912
+ [
913
+ 'date',
914
+ 'time',
915
+ 'datetime-local',
916
+ 'month',
917
+ 'week',
918
+ 'color',
919
+ 'range',
920
+ 'number',
921
+ ].includes(el.type)) {
947
922
  nativeSetValue(el, value);
948
- fire(el, 'input');
949
- fire(el, 'change');
923
+ fire(el, 'input', { inputType: 'insertText', data: value });
950
924
  }
951
925
  else {
952
- const sel = window.getSelection();
953
- if (sel) {
954
- sel.selectAllChildren(el);
955
- }
956
- let done;
957
926
  try {
958
- done = document.execCommand('insertText', false, value);
927
+ el.select();
959
928
  }
960
929
  catch (_error) {
961
- done = false;
930
+ /* ignore */
962
931
  }
963
- if (!done) {
964
- el.textContent = value;
965
- fire(el, 'input', { inputType: 'insertText', data: value });
932
+ fire(el, 'beforeinput', { inputType: 'insertText', data: value });
933
+ nativeSetValue(el, value);
934
+ try {
935
+ el.setSelectionRange(value.length, value.length);
966
936
  }
937
+ catch (_error) {
938
+ /* ignore */
939
+ }
940
+ fire(el, 'input', { inputType: 'insertText', data: value });
967
941
  }
968
- return { ok: true };
969
- }
970
- if (action === 'type') {
971
- ;
972
- el.focus();
973
- for (const ch of String(opts.text ?? ''))
974
- pressKey(el, ch === '\n' ? 'Enter' : ch);
975
- return { ok: true };
942
+ fire(el, 'change');
976
943
  }
977
- if (action === 'press') {
978
- ;
979
- el.focus();
980
- pressKey(el, String(opts.key ?? ''));
981
- return { ok: true };
944
+ else if (el instanceof HTMLSelectElement) {
945
+ nativeSetValue(el, value);
946
+ fire(el, 'input');
947
+ fire(el, 'change');
982
948
  }
983
- if (action === 'check' || action === 'uncheck' || action === 'setChecked') {
984
- if (!isCheckable(el))
985
- return { ok: false, error: describe(steps) + ' — not a checkbox, radio or switch' };
986
- const want = action === 'check' ? true : action === 'uncheck' ? false : Boolean(opts.checked);
987
- if (isChecked(el) !== want) {
988
- const p = pointAt(el);
989
- clickAt(el, p.x, p.y, {});
990
- if (isChecked(el) !== want)
991
- return {
992
- ok: false,
993
- error: describe(steps) + ' — clicking did not change its checked state',
994
- };
949
+ else {
950
+ const sel = window.getSelection();
951
+ if (sel) {
952
+ sel.selectAllChildren(el);
995
953
  }
996
- return { ok: true };
997
- }
998
- if (action === 'selectOption') {
999
- if (!(el instanceof HTMLSelectElement))
1000
- return { ok: false, error: describe(steps) + ' — not a <select>' };
1001
- const wanted = (Array.isArray(opts.values) ? opts.values : [opts.values]);
1002
- const chosen = [];
1003
- for (const option of Array.from(el.options)) {
1004
- const match = wanted.some((w) => {
1005
- if (w === null || w === undefined)
1006
- return false;
1007
- if (typeof w === 'string')
1008
- return (option.value === w ||
1009
- norm(option.label) === norm(w) ||
1010
- norm(option.textContent) === norm(w));
1011
- if (w.value !== undefined)
1012
- return option.value === w.value;
1013
- if (w.label !== undefined)
1014
- return norm(option.label) === norm(w.label);
1015
- if (w.index !== undefined)
1016
- return option.index === w.index;
1017
- return false;
1018
- });
1019
- option.selected = match;
1020
- if (match)
1021
- chosen.push(option.value);
954
+ let done;
955
+ try {
956
+ done = document.execCommand('insertText', false, value);
1022
957
  }
1023
- if (chosen.length === 0 && wanted.length)
958
+ catch (_error) {
959
+ done = false;
960
+ }
961
+ if (!done) {
962
+ el.textContent = value;
963
+ fire(el, 'input', { inputType: 'insertText', data: value });
964
+ }
965
+ }
966
+ return { ok: true };
967
+ }
968
+ if (action === 'type') {
969
+ ;
970
+ el.focus();
971
+ for (const ch of String(opts.text ?? ''))
972
+ pressKey(el, ch === '\n' ? 'Enter' : ch);
973
+ return { ok: true };
974
+ }
975
+ if (action === 'press') {
976
+ ;
977
+ el.focus();
978
+ pressKey(el, String(opts.key ?? ''));
979
+ return { ok: true };
980
+ }
981
+ if (action === 'check' || action === 'uncheck' || action === 'setChecked') {
982
+ if (!isCheckable(el))
983
+ return { ok: false, error: describe(steps) + ' — not a checkbox, radio or switch' };
984
+ const want = action === 'check' ? true : action === 'uncheck' ? false : Boolean(opts.checked);
985
+ if (isChecked(el) !== want) {
986
+ const p = pointAt(el);
987
+ clickAt(el, p.x, p.y, {});
988
+ if (isChecked(el) !== want)
1024
989
  return {
1025
990
  ok: false,
1026
- error: describe(steps) + ' — no option matched ' + JSON.stringify(wanted),
991
+ error: describe(steps) + ' — clicking did not change its checked state',
1027
992
  };
1028
- fire(el, 'input');
1029
- fire(el, 'change');
1030
- return { ok: true, values: chosen };
1031
993
  }
1032
- if (action === 'dispatchEvent') {
1033
- const type = String(opts.type ?? '');
1034
- const init = opts.init ?? {};
1035
- fire(el, type, init);
1036
- return { ok: true };
994
+ return { ok: true };
995
+ }
996
+ if (action === 'selectOption') {
997
+ if (!(el instanceof HTMLSelectElement))
998
+ return { ok: false, error: describe(steps) + ' — not a <select>' };
999
+ const wanted = (Array.isArray(opts.values) ? opts.values : [opts.values]);
1000
+ const chosen = [];
1001
+ for (const option of Array.from(el.options)) {
1002
+ const match = wanted.some((w) => {
1003
+ if (w === null || w === undefined)
1004
+ return false;
1005
+ if (typeof w === 'string')
1006
+ return (option.value === w ||
1007
+ norm(option.label) === norm(w) ||
1008
+ norm(option.textContent) === norm(w));
1009
+ if (w.value !== undefined)
1010
+ return option.value === w.value;
1011
+ if (w.label !== undefined)
1012
+ return norm(option.label) === norm(w.label);
1013
+ if (w.index !== undefined)
1014
+ return option.index === w.index;
1015
+ return false;
1016
+ });
1017
+ option.selected = match;
1018
+ if (match)
1019
+ chosen.push(option.value);
1037
1020
  }
1038
- if (action === 'highlight')
1039
- return { ok: true };
1040
- return { ok: false, error: 'unknown action: ' + action };
1021
+ if (chosen.length === 0 && wanted.length)
1022
+ return {
1023
+ ok: false,
1024
+ error: describe(steps) + ' — no option matched ' + JSON.stringify(wanted),
1025
+ };
1026
+ fire(el, 'input');
1027
+ fire(el, 'change');
1028
+ return { ok: true, values: chosen };
1041
1029
  }
1042
- if (Date.now() > deadline)
1043
- return { ok: false, error: reason ?? 'waiting for ' + describe(steps), timeout: true };
1044
- await sleep(50);
1030
+ if (action === 'dispatchEvent') {
1031
+ const type = String(opts.type ?? '');
1032
+ const init = opts.init ?? {};
1033
+ fire(el, type, init);
1034
+ return { ok: true };
1035
+ }
1036
+ if (action === 'highlight')
1037
+ return { ok: true };
1038
+ return { ok: false, error: 'unknown action: ' + action };
1045
1039
  }
1040
+ return { ok: false, error: reason ?? 'waiting for ' + describe(steps), retry: true };
1046
1041
  };
1047
1042
  // ---- reads (strict) ----
1048
- const single = async (steps, deadline, waitAttached) => {
1049
- for (;;) {
1050
- const els = resolveChain(steps);
1051
- if (els.length > 1)
1052
- return { res: { ok: false, strict: true, count: els.length } };
1053
- if (els.length === 1)
1054
- return { el: els[0] };
1055
- if (!waitAttached || Date.now() > deadline)
1056
- return {
1057
- res: {
1058
- ok: false,
1059
- error: describe(steps) + ' — element not found',
1060
- timeout: waitAttached,
1061
- },
1062
- };
1063
- await sleep(50);
1064
- }
1043
+ const single = async (steps, waitAttached) => {
1044
+ const els = resolveChain(steps);
1045
+ if (els.length > 1)
1046
+ return { res: { ok: false, strict: true, count: els.length } };
1047
+ if (els.length === 1)
1048
+ return { el: els[0] };
1049
+ return {
1050
+ res: {
1051
+ ok: false,
1052
+ error: describe(steps) + ' — element not found',
1053
+ retry: waitAttached,
1054
+ },
1055
+ };
1065
1056
  };
1066
1057
  const boundingBox = (el) => {
1067
1058
  if (!isVisible(el))
@@ -1069,7 +1060,7 @@ export function installE2ERuntime() {
1069
1060
  const r = el.getBoundingClientRect();
1070
1061
  return { x: r.x, y: r.y, width: r.width, height: r.height };
1071
1062
  };
1072
- const read = async (steps, what, args, deadline) => {
1063
+ const read = async (steps, what, args) => {
1073
1064
  if (what === 'isVisible' || what === 'isHidden') {
1074
1065
  const els = resolveChain(steps);
1075
1066
  if (els.length > 1)
@@ -1079,7 +1070,7 @@ export function installE2ERuntime() {
1079
1070
  }
1080
1071
  if (what === 'count')
1081
1072
  return { ok: true, value: resolveChain(steps).length };
1082
- const got = await single(steps, deadline, true);
1073
+ const got = await single(steps, true);
1083
1074
  if (got.res)
1084
1075
  return got.res;
1085
1076
  const el = got.el;
@@ -1135,8 +1126,8 @@ export function installE2ERuntime() {
1135
1126
  return { ok: false, error: 'unknown readAll: ' + what };
1136
1127
  };
1137
1128
  const runFn = (source) => new Function('return (' + source + ')')();
1138
- const evalOn = async (steps, source, arg, deadline) => {
1139
- const got = await single(steps, deadline, true);
1129
+ const evalOn = async (steps, source, arg) => {
1130
+ const got = await single(steps, true);
1140
1131
  if (got.res)
1141
1132
  return got.res;
1142
1133
  return { ok: true, value: await runFn(source)(got.el, arg) };
@@ -1145,27 +1136,23 @@ export function installE2ERuntime() {
1145
1136
  ok: true,
1146
1137
  value: await runFn(source)(resolveChain(steps), arg),
1147
1138
  });
1148
- const waitFor = async (steps, state, deadline) => {
1149
- for (;;) {
1150
- const els = resolveChain(steps);
1151
- if (els.length > 1 && state !== 'detached' && state !== 'hidden')
1152
- return { ok: false, strict: true, count: els.length };
1153
- const el = els[0];
1154
- const met = state === 'attached'
1155
- ? !!el
1156
- : state === 'detached'
1157
- ? !el
1158
- : state === 'visible'
1159
- ? !!el && isVisible(el)
1160
- : state === 'hidden'
1161
- ? !el || !isVisible(el)
1162
- : false;
1163
- if (met)
1164
- return { ok: true };
1165
- if (Date.now() > deadline)
1166
- return { ok: false, error: describe(steps) + ' — waiting for ' + state, timeout: true };
1167
- await sleep(50);
1168
- }
1139
+ const waitFor = async (steps, state) => {
1140
+ const els = resolveChain(steps);
1141
+ if (els.length > 1 && state !== 'detached' && state !== 'hidden')
1142
+ return { ok: false, strict: true, count: els.length };
1143
+ const el = els[0];
1144
+ const met = state === 'attached'
1145
+ ? !!el
1146
+ : state === 'detached'
1147
+ ? !el
1148
+ : state === 'visible'
1149
+ ? !!el && isVisible(el)
1150
+ : state === 'hidden'
1151
+ ? !el || !isVisible(el)
1152
+ : false;
1153
+ if (met)
1154
+ return { ok: true };
1155
+ return { ok: false, error: describe(steps) + ' — waiting for ' + state, retry: true };
1169
1156
  };
1170
1157
  // ---- expect probes: one evaluation, the driver polls ----
1171
1158
  const probe = (steps, matcher, args) => {
@@ -1457,22 +1444,21 @@ export function installE2ERuntime() {
1457
1444
  const call = async (req) => {
1458
1445
  const fn = String(req.fn);
1459
1446
  const a = req.args ?? [];
1460
- const deadline = Date.now() + Number(req.timeout ?? 10000);
1461
1447
  switch (fn) {
1462
1448
  case 'parse':
1463
1449
  return parseSelector(String(a[0]));
1464
1450
  case 'act':
1465
- return act(a[0], String(a[1]), a[2] ?? {}, deadline);
1451
+ return act(a[0], String(a[1]), a[2] ?? {});
1466
1452
  case 'read':
1467
- return read(a[0], String(a[1]), a[2] ?? [], deadline);
1453
+ return read(a[0], String(a[1]), a[2] ?? []);
1468
1454
  case 'readAll':
1469
1455
  return readAll(a[0], String(a[1]));
1470
1456
  case 'evalOn':
1471
- return evalOn(a[0], String(a[1]), a[2], deadline);
1457
+ return evalOn(a[0], String(a[1]), a[2]);
1472
1458
  case 'evalAll':
1473
1459
  return evalAll(a[0], String(a[1]), a[2]);
1474
1460
  case 'waitFor':
1475
- return waitFor(a[0], String(a[1]), deadline);
1461
+ return waitFor(a[0], String(a[1]));
1476
1462
  case 'probe':
1477
1463
  return probe(a[0], String(a[1]), a[2] ?? {});
1478
1464
  case 'describe':