@jsenv/core 28.1.0 → 28.1.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.
@@ -1,45 +1,35 @@
1
- let previousExecutionPromise; // https://twitter.com/damienmaillard/status/1554752482273787906
2
-
1
+ // https://twitter.com/damienmaillard/status/1554752482273787906
3
2
  const isWebkitOrSafari = typeof window.webkitConvertPointFromNodeToPage === "function";
4
3
  const superviseScriptTypeModule = async ({
5
4
  src,
6
5
  async
7
6
  }) => {
7
+ const currentScript = document.querySelector(`script[type="module"][inlined-from-src="${src}"]`);
8
8
  const execute = isWebkitOrSafari ? createExecuteWithDynamicImport({
9
9
  src
10
10
  }) : createExecuteWithScript({
11
+ currentScript,
11
12
  src
12
13
  });
13
14
 
14
- const startExecution = () => {
15
- const execution = window.__supervisor__.createExecution({
16
- src,
17
- type: "js_module",
18
- execute
19
- });
20
-
21
- return execution.start();
22
- };
23
-
24
- if (async) {
25
- startExecution();
26
- return;
27
- } // there is guaranteed execution order for non async script type="module"
28
- // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
29
-
30
-
31
- if (previousExecutionPromise) {
32
- await previousExecutionPromise;
33
- previousExecutionPromise = null;
15
+ if (!async) {
16
+ await window.__supervisor__.getPreviousExecutionDonePromise();
34
17
  }
35
18
 
36
- previousExecutionPromise = startExecution();
19
+ const execution = window.__supervisor__.createExecution({
20
+ src,
21
+ async,
22
+ type: "js_module",
23
+ execute
24
+ });
25
+
26
+ return execution.start();
37
27
  };
38
28
 
39
29
  const createExecuteWithScript = ({
30
+ currentScript,
40
31
  src
41
32
  }) => {
42
- const currentScript = document.querySelector(`script[type="module"][inlined-from-src="${src}"]`);
43
33
  const parentNode = currentScript.parentNode;
44
34
  let nodeToReplace;
45
35
  let currentScriptClone;
@@ -48,7 +38,10 @@ const createExecuteWithScript = ({
48
38
  }) => {
49
39
  const urlObject = new URL(src, window.location);
50
40
  const loadPromise = new Promise((resolve, reject) => {
51
- currentScriptClone = document.createElement("script");
41
+ currentScriptClone = document.createElement("script"); // browsers set async by default when creating script(s)
42
+ // we want an exact copy to preserves how code is executed
43
+
44
+ currentScriptClone.async = false;
52
45
  Array.from(currentScript.attributes).forEach(attribute => {
53
46
  currentScriptClone.setAttribute(attribute.nodeName, attribute.nodeValue);
54
47
  });
@@ -7,27 +7,17 @@ window.__supervisor__ = (() => {
7
7
  reportError: notImplemented,
8
8
  superviseScript: notImplemented,
9
9
  reloadSupervisedScript: notImplemented,
10
- collectScriptResults: notImplemented,
11
- getScriptExecutionResults: () => {
12
- // wait for page to load before collecting script execution results
13
- const htmlReadyPromise = new Promise((resolve) => {
14
- if (document.readyState === "complete") {
15
- resolve()
16
- return
17
- }
18
- const loadCallback = () => {
19
- window.removeEventListener("load", loadCallback)
20
- resolve()
21
- }
22
- window.addEventListener("load", loadCallback)
23
- })
24
- return htmlReadyPromise.then(() => {
25
- return supervisor.collectScriptResults()
26
- })
27
- },
10
+ getDocumentExecutionResult: notImplemented,
28
11
  executionResults,
29
12
  }
30
13
 
14
+ let navigationStartTime
15
+ try {
16
+ navigationStartTime = window.performance.timing.navigationStart
17
+ } catch (e) {
18
+ navigationStartTime = Date.now()
19
+ }
20
+
31
21
  supervisor.setupReportException = ({
32
22
  rootDirectoryUrl,
33
23
  errorNotification,
@@ -728,11 +718,12 @@ window.__supervisor__ = (() => {
728
718
  })
729
719
 
730
720
  const supervisedScripts = []
731
- const executionPromises = []
732
- supervisor.createExecution = ({ type, src, execute }) => {
721
+ const scriptExecutionPromises = []
722
+ supervisor.createExecution = ({ type, src, async, execute }) => {
733
723
  const execution = {
734
724
  type,
735
725
  src,
726
+ async,
736
727
  execute,
737
728
  }
738
729
  execution.start = () => {
@@ -753,21 +744,27 @@ window.__supervisor__ = (() => {
753
744
  }
754
745
  const executionResult = {
755
746
  status: "pending",
747
+ startTime: Date.now(),
748
+ endTime: null,
756
749
  exception: null,
757
750
  namespace: null,
758
751
  coverage: null,
759
752
  }
760
753
  executionResults[execution.src] = executionResult
761
- let resolvePromise
762
- const promise = new Promise((resolve) => {
763
- resolvePromise = resolve
754
+ let resolveScriptExecutionPromise
755
+ const scriptExecutionPromise = new Promise((resolve) => {
756
+ resolveScriptExecutionPromise = () => {
757
+ executionResult.endTime = Date.now()
758
+ if (measurePerf) {
759
+ performance.measure(`execution`, `execution_start`)
760
+ }
761
+ resolve()
762
+ }
764
763
  })
765
- executionPromises.push(promise)
764
+ scriptExecutionPromise.execution = execution
765
+ scriptExecutionPromises.push(scriptExecutionPromise)
766
766
  try {
767
767
  const result = await execution.execute({ isReload })
768
- if (measurePerf) {
769
- performance.measure(`execution`, `execution_start`)
770
- }
771
768
  executionResult.status = "completed"
772
769
  executionResult.namespace = result
773
770
  executionResult.coverage = window.__coverage__
@@ -775,11 +772,8 @@ window.__supervisor__ = (() => {
775
772
  console.log(`${execution.type} load ended`)
776
773
  console.groupEnd()
777
774
  }
778
- resolvePromise()
775
+ resolveScriptExecutionPromise()
779
776
  } catch (e) {
780
- if (measurePerf) {
781
- performance.measure(`execution`, `execution_start`)
782
- }
783
777
  executionResult.status = "errored"
784
778
  const exception = supervisor.createException({
785
779
  reason: e,
@@ -792,73 +786,79 @@ window.__supervisor__ = (() => {
792
786
  if (logs) {
793
787
  console.groupEnd()
794
788
  }
795
- resolvePromise()
789
+ resolveScriptExecutionPromise()
796
790
  }
797
791
  }
798
- let previousExecutionPromise
792
+
793
+ // respect execution order
794
+ // - wait for classic scripts to be done (non async)
795
+ // - wait module script previous execution (non async)
796
+ // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
797
+ supervisor.getPreviousExecutionDonePromise = async () => {
798
+ const previousNonAsyncScriptExecutions = scriptExecutionPromises.filter(
799
+ (promise) => !promise.execution.async,
800
+ )
801
+ await Promise.all(previousNonAsyncScriptExecutions)
802
+ }
799
803
  supervisor.superviseScript = async ({ src, async }) => {
800
804
  const { currentScript } = document
801
805
  const parentNode = currentScript.parentNode
802
- const startExecution = () => {
803
- let nodeToReplace
804
- let currentScriptClone
805
- const execution = supervisor.createExecution({
806
- src,
807
- type: "js_classic",
808
- execute: async ({ isReload }) => {
809
- const urlObject = new URL(src, window.location)
810
- const loadPromise = new Promise((resolve, reject) => {
811
- // do not use script.cloneNode()
812
- // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
813
- currentScriptClone = document.createElement("script")
814
- Array.from(currentScript.attributes).forEach((attribute) => {
815
- currentScriptClone.setAttribute(
816
- attribute.nodeName,
817
- attribute.nodeValue,
818
- )
819
- })
820
- if (isReload) {
821
- urlObject.searchParams.set("hmr", Date.now())
822
- nodeToReplace = currentScriptClone
823
- currentScriptClone.src = urlObject.href
824
- } else {
825
- currentScriptClone.removeAttribute("jsenv-plugin-owner")
826
- currentScriptClone.removeAttribute("jsenv-plugin-action")
827
- currentScriptClone.removeAttribute("inlined-from-src")
828
- currentScriptClone.removeAttribute("original-position")
829
- currentScriptClone.removeAttribute("original-src-position")
830
- nodeToReplace = currentScript
831
- currentScriptClone.src = src
832
- }
833
- currentScriptClone.addEventListener("error", reject)
834
- currentScriptClone.addEventListener("load", resolve)
835
- parentNode.replaceChild(currentScriptClone, nodeToReplace)
806
+ if (!async) {
807
+ await supervisor.getPreviousExecutionDonePromise()
808
+ }
809
+ let nodeToReplace
810
+ let currentScriptClone
811
+ const execution = supervisor.createExecution({
812
+ src,
813
+ type: "js_classic",
814
+ async,
815
+ execute: async ({ isReload }) => {
816
+ const urlObject = new URL(src, window.location)
817
+ const loadPromise = new Promise((resolve, reject) => {
818
+ // do not use script.cloneNode()
819
+ // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
820
+ currentScriptClone = document.createElement("script")
821
+ // browsers set async by default when creating script(s)
822
+ // we want an exact copy to preserves how code is executed
823
+ currentScriptClone.async = false
824
+ Array.from(currentScript.attributes).forEach((attribute) => {
825
+ currentScriptClone.setAttribute(
826
+ attribute.nodeName,
827
+ attribute.nodeValue,
828
+ )
836
829
  })
837
- try {
838
- await loadPromise
839
- } catch (e) {
840
- // eslint-disable-next-line no-throw-literal
841
- throw {
842
- message: `Failed to fetch script: ${urlObject.href}`,
843
- reportedBy: "script_error_event",
844
- url: urlObject.href,
845
- // window.error won't be dispatched for this error
846
- needsReport: true,
847
- }
830
+ if (isReload) {
831
+ urlObject.searchParams.set("hmr", Date.now())
832
+ nodeToReplace = currentScriptClone
833
+ currentScriptClone.src = urlObject.href
834
+ } else {
835
+ currentScriptClone.removeAttribute("jsenv-plugin-owner")
836
+ currentScriptClone.removeAttribute("jsenv-plugin-action")
837
+ currentScriptClone.removeAttribute("inlined-from-src")
838
+ currentScriptClone.removeAttribute("original-position")
839
+ currentScriptClone.removeAttribute("original-src-position")
840
+ nodeToReplace = currentScript
841
+ currentScriptClone.src = src
848
842
  }
849
- },
850
- })
851
- return execution.start()
852
- }
853
- if (async) {
854
- startExecution()
855
- return
856
- }
857
- if (previousExecutionPromise) {
858
- await previousExecutionPromise
859
- previousExecutionPromise = null
860
- }
861
- previousExecutionPromise = startExecution()
843
+ currentScriptClone.addEventListener("error", reject)
844
+ currentScriptClone.addEventListener("load", resolve)
845
+ parentNode.replaceChild(currentScriptClone, nodeToReplace)
846
+ })
847
+ try {
848
+ await loadPromise
849
+ } catch (e) {
850
+ // eslint-disable-next-line no-throw-literal
851
+ throw {
852
+ message: `Failed to fetch script: ${urlObject.href}`,
853
+ reportedBy: "script_error_event",
854
+ url: urlObject.href,
855
+ // window.error won't be dispatched for this error
856
+ needsReport: true,
857
+ }
858
+ }
859
+ },
860
+ })
861
+ return execution.start()
862
862
  }
863
863
  supervisor.reloadSupervisedScript = ({ type, src }) => {
864
864
  const supervisedScript = supervisedScripts.find(
@@ -876,45 +876,39 @@ window.__supervisor__ = (() => {
876
876
  supervisedScript.reload()
877
877
  }
878
878
  }
879
- supervisor.collectScriptResults = async () => {
879
+ supervisor.getDocumentExecutionResult = async () => {
880
880
  // just to be super safe and ensure any <script type="module"> got a chance to execute
881
- const scriptTypeModuleLoaded = new Promise((resolve) => {
882
- const scriptTypeModule = document.createElement("script")
883
- scriptTypeModule.type = "module"
884
- scriptTypeModule.innerText =
885
- "window.__supervisor__.scriptModuleCallback()"
886
- window.__supervisor__.scriptModuleCallback = () => {
887
- scriptTypeModule.parentNode.removeChild(scriptTypeModule)
881
+ const documentReadyPromise = new Promise((resolve) => {
882
+ if (document.readyState === "complete") {
888
883
  resolve()
884
+ return
889
885
  }
890
- document.body.appendChild(scriptTypeModule)
886
+ const loadCallback = () => {
887
+ window.removeEventListener("load", loadCallback)
888
+ resolve()
889
+ }
890
+ window.addEventListener("load", loadCallback)
891
891
  })
892
- await scriptTypeModuleLoaded
893
-
894
- const waitPendingExecutions = async () => {
895
- if (executionPromises.length) {
896
- const promisesToWait = executionPromises.slice()
897
- executionPromises.length = 0
898
- await Promise.all(promisesToWait)
899
- await waitPendingExecutions()
892
+ await documentReadyPromise
893
+ const waitScriptExecutions = async () => {
894
+ const numberOfScripts = scriptExecutionPromises.length
895
+ await Promise.all(scriptExecutionPromises)
896
+ // new scripts added while the other where executing
897
+ // (should happen only on webkit where
898
+ // script might be added after window load event)
899
+ if (scriptExecutionPromises.length > numberOfScripts) {
900
+ await waitScriptExecutions()
900
901
  }
901
902
  }
902
- await waitPendingExecutions()
903
+ await waitScriptExecutions()
904
+
903
905
  return {
904
906
  status: "completed",
905
907
  executionResults,
906
- startTime: getNavigationStartTime(),
908
+ startTime: navigationStartTime,
907
909
  endTime: Date.now(),
908
910
  }
909
911
  }
910
-
911
- const getNavigationStartTime = () => {
912
- try {
913
- return window.performance.timing.navigationStart
914
- } catch (e) {
915
- return Date.now()
916
- }
917
- }
918
912
  }
919
913
 
920
914
  return supervisor
package/dist/main.js CHANGED
@@ -1320,29 +1320,33 @@ const createOperation = () => {
1320
1320
  };
1321
1321
 
1322
1322
  const addAbortSource = abortSourceCallback => {
1323
+ const abortSource = {
1324
+ cleaned: false,
1325
+ signal: null,
1326
+ remove: callbackNoop
1327
+ };
1323
1328
  const abortSourceController = new AbortController();
1324
1329
  const abortSourceSignal = abortSourceController.signal;
1330
+ abortSource.signal = abortSourceSignal;
1325
1331
 
1326
1332
  if (operationSignal.aborted) {
1327
- return {
1328
- signal: abortSourceSignal,
1329
- remove: callbackNoop
1330
- };
1333
+ return abortSource;
1331
1334
  }
1332
1335
 
1333
1336
  const returnValue = abortSourceCallback(value => {
1334
1337
  abortSourceController.abort(value);
1335
1338
  });
1336
- const removeAbortSource = typeof returnValue === "function" ? returnValue : callbackNoop;
1337
1339
  const removeAbortSignal = addAbortSignal(abortSourceSignal, {
1338
1340
  onRemove: () => {
1339
- removeAbortSource();
1341
+ if (typeof returnValue === "function") {
1342
+ returnValue();
1343
+ }
1344
+
1345
+ abortSource.cleaned = true;
1340
1346
  }
1341
1347
  });
1342
- return {
1343
- signal: abortSourceSignal,
1344
- remove: removeAbortSignal
1345
- };
1348
+ abortSource.remove = removeAbortSignal;
1349
+ return abortSource;
1346
1350
  };
1347
1351
 
1348
1352
  const timeout = ms => {
@@ -26503,7 +26507,7 @@ const run = async ({
26503
26507
  } catch (e) {
26504
26508
  cb({
26505
26509
  status: "errored",
26506
- error: e
26510
+ errors: [e]
26507
26511
  });
26508
26512
  }
26509
26513
  }
@@ -27575,7 +27579,7 @@ const executeTestPlan = async ({
27575
27579
  testPlan,
27576
27580
  updateProcessExitCode = true,
27577
27581
  maxExecutionsInParallel = 1,
27578
- defaultMsAllocatedPerExecution = 30000,
27582
+ defaultMsAllocatedPerExecution = 30_000,
27579
27583
  failFast = false,
27580
27584
  // keepRunning: false to ensure runtime is stopped once executed
27581
27585
  // because we have what we wants: execution is completed and
@@ -28036,7 +28040,7 @@ const createRuntimeFromPlaywright = ({
28036
28040
  throw new Error(`window.__supervisor__ not found`);
28037
28041
  }
28038
28042
 
28039
- return window.__supervisor__.getScriptExecutionResults();
28043
+ return window.__supervisor__.getDocumentExecutionResult();
28040
28044
  }
28041
28045
  /* eslint-enable no-undef */
28042
28046
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "28.1.0",
3
+ "version": "28.1.3",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -65,15 +65,15 @@
65
65
  "@babel/plugin-transform-modules-umd": "7.18.6",
66
66
  "@c88/v8-coverage": "0.1.1",
67
67
  "@financial-times/polyfill-useragent-normaliser": "2.0.1",
68
- "@jsenv/abort": "4.2.3",
68
+ "@jsenv/abort": "4.2.4",
69
69
  "@jsenv/ast": "1.2.0",
70
70
  "@jsenv/babel-plugins": "1.0.6",
71
- "@jsenv/filesystem": "4.1.2",
71
+ "@jsenv/filesystem": "4.1.3",
72
72
  "@jsenv/importmap": "1.2.1",
73
73
  "@jsenv/integrity": "0.0.1",
74
74
  "@jsenv/log": "3.2.0",
75
75
  "@jsenv/node-esm-resolution": "0.1.0",
76
- "@jsenv/server": "14.1.1",
76
+ "@jsenv/server": "14.1.2",
77
77
  "@jsenv/sourcemap": "1.0.4",
78
78
  "@jsenv/uneval": "1.6.0",
79
79
  "@jsenv/url-meta": "7.0.0",
@@ -108,7 +108,7 @@ export const run = async ({
108
108
  } catch (e) {
109
109
  cb({
110
110
  status: "errored",
111
- error: e,
111
+ errors: [e],
112
112
  })
113
113
  }
114
114
  },
@@ -306,7 +306,7 @@ export const createRuntimeFromPlaywright = ({
306
306
  if (!window.__supervisor__) {
307
307
  throw new Error(`window.__supervisor__ not found`)
308
308
  }
309
- return window.__supervisor__.getScriptExecutionResults()
309
+ return window.__supervisor__.getDocumentExecutionResult()
310
310
  },
311
311
  /* eslint-enable no-undef */
312
312
  )
@@ -1,46 +1,37 @@
1
- let previousExecutionPromise
2
-
3
1
  // https://twitter.com/damienmaillard/status/1554752482273787906
4
2
  const isWebkitOrSafari =
5
3
  typeof window.webkitConvertPointFromNodeToPage === "function"
6
4
 
7
5
  export const superviseScriptTypeModule = async ({ src, async }) => {
6
+ const currentScript = document.querySelector(
7
+ `script[type="module"][inlined-from-src="${src}"]`,
8
+ )
8
9
  const execute = isWebkitOrSafari
9
10
  ? createExecuteWithDynamicImport({ src })
10
- : createExecuteWithScript({ src })
11
- const startExecution = () => {
12
- const execution = window.__supervisor__.createExecution({
13
- src,
14
- type: "js_module",
15
- execute,
16
- })
17
- return execution.start()
18
- }
19
- if (async) {
20
- startExecution()
21
- return
11
+ : createExecuteWithScript({ currentScript, src })
12
+ if (!async) {
13
+ await window.__supervisor__.getPreviousExecutionDonePromise()
22
14
  }
23
- // there is guaranteed execution order for non async script type="module"
24
- // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
25
- if (previousExecutionPromise) {
26
- await previousExecutionPromise
27
- previousExecutionPromise = null
28
- }
29
- previousExecutionPromise = startExecution()
15
+ const execution = window.__supervisor__.createExecution({
16
+ src,
17
+ async,
18
+ type: "js_module",
19
+ execute,
20
+ })
21
+ return execution.start()
30
22
  }
31
23
 
32
- const createExecuteWithScript = ({ src }) => {
33
- const currentScript = document.querySelector(
34
- `script[type="module"][inlined-from-src="${src}"]`,
35
- )
24
+ const createExecuteWithScript = ({ currentScript, src }) => {
36
25
  const parentNode = currentScript.parentNode
37
26
  let nodeToReplace
38
27
  let currentScriptClone
39
-
40
28
  return async ({ isReload }) => {
41
29
  const urlObject = new URL(src, window.location)
42
30
  const loadPromise = new Promise((resolve, reject) => {
43
31
  currentScriptClone = document.createElement("script")
32
+ // browsers set async by default when creating script(s)
33
+ // we want an exact copy to preserves how code is executed
34
+ currentScriptClone.async = false
44
35
  Array.from(currentScript.attributes).forEach((attribute) => {
45
36
  currentScriptClone.setAttribute(attribute.nodeName, attribute.nodeValue)
46
37
  })
@@ -7,27 +7,17 @@ window.__supervisor__ = (() => {
7
7
  reportError: notImplemented,
8
8
  superviseScript: notImplemented,
9
9
  reloadSupervisedScript: notImplemented,
10
- collectScriptResults: notImplemented,
11
- getScriptExecutionResults: () => {
12
- // wait for page to load before collecting script execution results
13
- const htmlReadyPromise = new Promise((resolve) => {
14
- if (document.readyState === "complete") {
15
- resolve()
16
- return
17
- }
18
- const loadCallback = () => {
19
- window.removeEventListener("load", loadCallback)
20
- resolve()
21
- }
22
- window.addEventListener("load", loadCallback)
23
- })
24
- return htmlReadyPromise.then(() => {
25
- return supervisor.collectScriptResults()
26
- })
27
- },
10
+ getDocumentExecutionResult: notImplemented,
28
11
  executionResults,
29
12
  }
30
13
 
14
+ let navigationStartTime
15
+ try {
16
+ navigationStartTime = window.performance.timing.navigationStart
17
+ } catch (e) {
18
+ navigationStartTime = Date.now()
19
+ }
20
+
31
21
  supervisor.setupReportException = ({
32
22
  rootDirectoryUrl,
33
23
  errorNotification,
@@ -728,11 +718,12 @@ window.__supervisor__ = (() => {
728
718
  })
729
719
 
730
720
  const supervisedScripts = []
731
- const executionPromises = []
732
- supervisor.createExecution = ({ type, src, execute }) => {
721
+ const scriptExecutionPromises = []
722
+ supervisor.createExecution = ({ type, src, async, execute }) => {
733
723
  const execution = {
734
724
  type,
735
725
  src,
726
+ async,
736
727
  execute,
737
728
  }
738
729
  execution.start = () => {
@@ -753,21 +744,27 @@ window.__supervisor__ = (() => {
753
744
  }
754
745
  const executionResult = {
755
746
  status: "pending",
747
+ startTime: Date.now(),
748
+ endTime: null,
756
749
  exception: null,
757
750
  namespace: null,
758
751
  coverage: null,
759
752
  }
760
753
  executionResults[execution.src] = executionResult
761
- let resolvePromise
762
- const promise = new Promise((resolve) => {
763
- resolvePromise = resolve
754
+ let resolveScriptExecutionPromise
755
+ const scriptExecutionPromise = new Promise((resolve) => {
756
+ resolveScriptExecutionPromise = () => {
757
+ executionResult.endTime = Date.now()
758
+ if (measurePerf) {
759
+ performance.measure(`execution`, `execution_start`)
760
+ }
761
+ resolve()
762
+ }
764
763
  })
765
- executionPromises.push(promise)
764
+ scriptExecutionPromise.execution = execution
765
+ scriptExecutionPromises.push(scriptExecutionPromise)
766
766
  try {
767
767
  const result = await execution.execute({ isReload })
768
- if (measurePerf) {
769
- performance.measure(`execution`, `execution_start`)
770
- }
771
768
  executionResult.status = "completed"
772
769
  executionResult.namespace = result
773
770
  executionResult.coverage = window.__coverage__
@@ -775,11 +772,8 @@ window.__supervisor__ = (() => {
775
772
  console.log(`${execution.type} load ended`)
776
773
  console.groupEnd()
777
774
  }
778
- resolvePromise()
775
+ resolveScriptExecutionPromise()
779
776
  } catch (e) {
780
- if (measurePerf) {
781
- performance.measure(`execution`, `execution_start`)
782
- }
783
777
  executionResult.status = "errored"
784
778
  const exception = supervisor.createException({
785
779
  reason: e,
@@ -792,73 +786,79 @@ window.__supervisor__ = (() => {
792
786
  if (logs) {
793
787
  console.groupEnd()
794
788
  }
795
- resolvePromise()
789
+ resolveScriptExecutionPromise()
796
790
  }
797
791
  }
798
- let previousExecutionPromise
792
+
793
+ // respect execution order
794
+ // - wait for classic scripts to be done (non async)
795
+ // - wait module script previous execution (non async)
796
+ // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
797
+ supervisor.getPreviousExecutionDonePromise = async () => {
798
+ const previousNonAsyncScriptExecutions = scriptExecutionPromises.filter(
799
+ (promise) => !promise.execution.async,
800
+ )
801
+ await Promise.all(previousNonAsyncScriptExecutions)
802
+ }
799
803
  supervisor.superviseScript = async ({ src, async }) => {
800
804
  const { currentScript } = document
801
805
  const parentNode = currentScript.parentNode
802
- const startExecution = () => {
803
- let nodeToReplace
804
- let currentScriptClone
805
- const execution = supervisor.createExecution({
806
- src,
807
- type: "js_classic",
808
- execute: async ({ isReload }) => {
809
- const urlObject = new URL(src, window.location)
810
- const loadPromise = new Promise((resolve, reject) => {
811
- // do not use script.cloneNode()
812
- // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
813
- currentScriptClone = document.createElement("script")
814
- Array.from(currentScript.attributes).forEach((attribute) => {
815
- currentScriptClone.setAttribute(
816
- attribute.nodeName,
817
- attribute.nodeValue,
818
- )
819
- })
820
- if (isReload) {
821
- urlObject.searchParams.set("hmr", Date.now())
822
- nodeToReplace = currentScriptClone
823
- currentScriptClone.src = urlObject.href
824
- } else {
825
- currentScriptClone.removeAttribute("jsenv-plugin-owner")
826
- currentScriptClone.removeAttribute("jsenv-plugin-action")
827
- currentScriptClone.removeAttribute("inlined-from-src")
828
- currentScriptClone.removeAttribute("original-position")
829
- currentScriptClone.removeAttribute("original-src-position")
830
- nodeToReplace = currentScript
831
- currentScriptClone.src = src
832
- }
833
- currentScriptClone.addEventListener("error", reject)
834
- currentScriptClone.addEventListener("load", resolve)
835
- parentNode.replaceChild(currentScriptClone, nodeToReplace)
806
+ if (!async) {
807
+ await supervisor.getPreviousExecutionDonePromise()
808
+ }
809
+ let nodeToReplace
810
+ let currentScriptClone
811
+ const execution = supervisor.createExecution({
812
+ src,
813
+ type: "js_classic",
814
+ async,
815
+ execute: async ({ isReload }) => {
816
+ const urlObject = new URL(src, window.location)
817
+ const loadPromise = new Promise((resolve, reject) => {
818
+ // do not use script.cloneNode()
819
+ // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
820
+ currentScriptClone = document.createElement("script")
821
+ // browsers set async by default when creating script(s)
822
+ // we want an exact copy to preserves how code is executed
823
+ currentScriptClone.async = false
824
+ Array.from(currentScript.attributes).forEach((attribute) => {
825
+ currentScriptClone.setAttribute(
826
+ attribute.nodeName,
827
+ attribute.nodeValue,
828
+ )
836
829
  })
837
- try {
838
- await loadPromise
839
- } catch (e) {
840
- // eslint-disable-next-line no-throw-literal
841
- throw {
842
- message: `Failed to fetch script: ${urlObject.href}`,
843
- reportedBy: "script_error_event",
844
- url: urlObject.href,
845
- // window.error won't be dispatched for this error
846
- needsReport: true,
847
- }
830
+ if (isReload) {
831
+ urlObject.searchParams.set("hmr", Date.now())
832
+ nodeToReplace = currentScriptClone
833
+ currentScriptClone.src = urlObject.href
834
+ } else {
835
+ currentScriptClone.removeAttribute("jsenv-plugin-owner")
836
+ currentScriptClone.removeAttribute("jsenv-plugin-action")
837
+ currentScriptClone.removeAttribute("inlined-from-src")
838
+ currentScriptClone.removeAttribute("original-position")
839
+ currentScriptClone.removeAttribute("original-src-position")
840
+ nodeToReplace = currentScript
841
+ currentScriptClone.src = src
848
842
  }
849
- },
850
- })
851
- return execution.start()
852
- }
853
- if (async) {
854
- startExecution()
855
- return
856
- }
857
- if (previousExecutionPromise) {
858
- await previousExecutionPromise
859
- previousExecutionPromise = null
860
- }
861
- previousExecutionPromise = startExecution()
843
+ currentScriptClone.addEventListener("error", reject)
844
+ currentScriptClone.addEventListener("load", resolve)
845
+ parentNode.replaceChild(currentScriptClone, nodeToReplace)
846
+ })
847
+ try {
848
+ await loadPromise
849
+ } catch (e) {
850
+ // eslint-disable-next-line no-throw-literal
851
+ throw {
852
+ message: `Failed to fetch script: ${urlObject.href}`,
853
+ reportedBy: "script_error_event",
854
+ url: urlObject.href,
855
+ // window.error won't be dispatched for this error
856
+ needsReport: true,
857
+ }
858
+ }
859
+ },
860
+ })
861
+ return execution.start()
862
862
  }
863
863
  supervisor.reloadSupervisedScript = ({ type, src }) => {
864
864
  const supervisedScript = supervisedScripts.find(
@@ -876,45 +876,39 @@ window.__supervisor__ = (() => {
876
876
  supervisedScript.reload()
877
877
  }
878
878
  }
879
- supervisor.collectScriptResults = async () => {
879
+ supervisor.getDocumentExecutionResult = async () => {
880
880
  // just to be super safe and ensure any <script type="module"> got a chance to execute
881
- const scriptTypeModuleLoaded = new Promise((resolve) => {
882
- const scriptTypeModule = document.createElement("script")
883
- scriptTypeModule.type = "module"
884
- scriptTypeModule.innerText =
885
- "window.__supervisor__.scriptModuleCallback()"
886
- window.__supervisor__.scriptModuleCallback = () => {
887
- scriptTypeModule.parentNode.removeChild(scriptTypeModule)
881
+ const documentReadyPromise = new Promise((resolve) => {
882
+ if (document.readyState === "complete") {
888
883
  resolve()
884
+ return
889
885
  }
890
- document.body.appendChild(scriptTypeModule)
886
+ const loadCallback = () => {
887
+ window.removeEventListener("load", loadCallback)
888
+ resolve()
889
+ }
890
+ window.addEventListener("load", loadCallback)
891
891
  })
892
- await scriptTypeModuleLoaded
893
-
894
- const waitPendingExecutions = async () => {
895
- if (executionPromises.length) {
896
- const promisesToWait = executionPromises.slice()
897
- executionPromises.length = 0
898
- await Promise.all(promisesToWait)
899
- await waitPendingExecutions()
892
+ await documentReadyPromise
893
+ const waitScriptExecutions = async () => {
894
+ const numberOfScripts = scriptExecutionPromises.length
895
+ await Promise.all(scriptExecutionPromises)
896
+ // new scripts added while the other where executing
897
+ // (should happen only on webkit where
898
+ // script might be added after window load event)
899
+ if (scriptExecutionPromises.length > numberOfScripts) {
900
+ await waitScriptExecutions()
900
901
  }
901
902
  }
902
- await waitPendingExecutions()
903
+ await waitScriptExecutions()
904
+
903
905
  return {
904
906
  status: "completed",
905
907
  executionResults,
906
- startTime: getNavigationStartTime(),
908
+ startTime: navigationStartTime,
907
909
  endTime: Date.now(),
908
910
  }
909
911
  }
910
-
911
- const getNavigationStartTime = () => {
912
- try {
913
- return window.performance.timing.navigationStart
914
- } catch (e) {
915
- return Date.now()
916
- }
917
- }
918
912
  }
919
913
 
920
914
  return supervisor
@@ -9,7 +9,7 @@ export const renderExecutionInToolbar = async () => {
9
9
  removeForceHideElement(document.querySelector("#execution-indicator"))
10
10
 
11
11
  const { status, startTime, endTime } =
12
- await window.parent.__supervisor__.getScriptExecutionResults()
12
+ await window.parent.__supervisor__.getDocumentExecutionResult()
13
13
  const execution = { status, startTime, endTime }
14
14
  applyExecutionIndicator(execution)
15
15
  const executionStorageKey = window.location.href
@@ -52,7 +52,7 @@ export const executeTestPlan = async ({
52
52
  testPlan,
53
53
  updateProcessExitCode = true,
54
54
  maxExecutionsInParallel = 1,
55
- defaultMsAllocatedPerExecution = 30000,
55
+ defaultMsAllocatedPerExecution = 30_000,
56
56
  failFast = false,
57
57
  // keepRunning: false to ensure runtime is stopped once executed
58
58
  // because we have what we wants: execution is completed and