@lvce-editor/test-with-playwright-worker 22.39.2 → 22.39.4

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,8 +1,8 @@
1
+ import IstanbulCoverage from 'istanbul-lib-coverage';
1
2
  import IstanbulReport from 'istanbul-lib-report';
2
3
  import IstanbulReports from 'istanbul-reports';
3
4
  import { rm, mkdir, readFile } from 'node:fs/promises';
4
5
  import { join } from 'node:path';
5
- import IstanbulCoverage from 'istanbul-lib-coverage';
6
6
  import v8ToIstanbul from 'v8-to-istanbul';
7
7
 
8
8
  const normalizeLine = line => {
@@ -876,9 +876,24 @@ const create = async ({
876
876
  const WriteJavascriptCoverage = 'WriteJavascriptCoverage';
877
877
 
878
878
  const externalSourceMapCommentRegex = /(?:\/\/[#@]\s*sourceMappingURL=(?!data:).*?$|\/\*[#@]\s*sourceMappingURL=(?!data:).*?\*\/)/gm;
879
+ const externalSourceMapUrlRegex = /(?:\/\/[#@]|\/\*[#@])\s*sourceMappingURL=(?!data:)([^\s*]+)/;
879
880
  const temporaryServerRootRegex = /^\/[a-f\d]{7,}(?=\/(?:js|packages)\/)/;
881
+ const windowsDrivePrefixRegex = /^[a-z]:/i;
882
+ const getExternalSourceMap = async (source, url) => {
883
+ const sourceMapUrl = source.match(externalSourceMapUrlRegex)?.[1];
884
+ if (!sourceMapUrl) {
885
+ return undefined;
886
+ }
887
+ const response = await fetch(new URL(sourceMapUrl, url));
888
+ if (!response.ok) {
889
+ throw new Error(`[test-with-playwright] failed to fetch worker source map ${sourceMapUrl}: ${response.status}`);
890
+ }
891
+ return {
892
+ sourcemap: await response.json()
893
+ };
894
+ };
880
895
  const normalizeCoveragePath = path => {
881
- return path.replace(temporaryServerRootRegex, '');
896
+ return path.replaceAll('\\', '/').replace(windowsDrivePrefixRegex, '').replace(temporaryServerRootRegex, '');
882
897
  };
883
898
  const normalizeCoverageData = coverageData => {
884
899
  const normalized = Object.create(null);
@@ -909,9 +924,13 @@ const addEntryToCoverageMap = async (coverageMap, entry) => {
909
924
  if (!entry.source || !entry.url || isTestScript(entry.url)) {
910
925
  return;
911
926
  }
927
+ const sourceMap = await getExternalSourceMap(entry.source, entry.url);
912
928
  const source = entry.source.replaceAll(externalSourceMapCommentRegex, '');
913
929
  const converter = v8ToIstanbul(getCoveragePath(entry.url), 0, {
914
- source
930
+ source,
931
+ ...(sourceMap && {
932
+ sourceMap
933
+ })
915
934
  });
916
935
  await converter.load();
917
936
  converter.applyCoverage(entry.functions);
@@ -932,8 +951,29 @@ const executeReport = (coverageMap, directory, name) => {
932
951
  });
933
952
  IstanbulReports.create(name).execute(context);
934
953
  };
935
- const writeJavascriptCoverage = async (entries, directory) => {
936
- const coverageMap = await createJavascriptCoverage(entries);
954
+ const writeJavascriptCoverage = async (entries, directory, threshold, include) => {
955
+ let coverageMap = await createJavascriptCoverage(entries);
956
+ if (include) {
957
+ const includedCoverage = IstanbulCoverage.createCoverageMap();
958
+ for (const path of coverageMap.files()) {
959
+ if (path.includes(include)) {
960
+ includedCoverage.addFileCoverage(coverageMap.fileCoverageFor(path));
961
+ }
962
+ }
963
+ coverageMap = includedCoverage;
964
+ if (coverageMap.files().length === 0) {
965
+ throw new Error(`[test-with-playwright] worker coverage target produced no files matching ${include}`);
966
+ }
967
+ }
968
+ if (threshold !== undefined) {
969
+ if (coverageMap.files().length === 0) {
970
+ throw new Error('[test-with-playwright] worker coverage target produced no reportable JavaScript');
971
+ }
972
+ const {
973
+ lines
974
+ } = coverageMap.getCoverageSummary().toJSON();
975
+ console.info(`[test-with-playwright] worker line coverage: ${lines.pct}% (${lines.covered}/${lines.total})`);
976
+ }
937
977
  await rm(directory, {
938
978
  force: true,
939
979
  recursive: true
@@ -954,6 +994,14 @@ const writeJavascriptCoverage = async (entries, directory) => {
954
994
  const summaryText = await readFile(join(directory, 'coverage.txt'), 'utf8');
955
995
  const summary = summaryText.trimEnd();
956
996
  console.info(`[test-with-playwright] JavaScript coverage written to ${directory}\n${summary}`);
997
+ if (threshold !== undefined) {
998
+ const {
999
+ lines
1000
+ } = coverageMap.getCoverageSummary().toJSON();
1001
+ if (lines.total === 0 || lines.pct < threshold) {
1002
+ throw new Error(`[test-with-playwright] worker line coverage ${lines.pct}% is below threshold ${threshold}%`);
1003
+ }
1004
+ }
957
1005
  };
958
1006
 
959
1007
  const commandMap = {
@@ -2099,37 +2099,290 @@ const getCoverageWorkerUrl = (moduleUrl = import.meta.url) => {
2099
2099
  return new URL('../../../../test-with-playwright-coverage-worker/src/workerMain.ts', moduleUrl).href;
2100
2100
  };
2101
2101
 
2102
- const writeJavascriptCoverage = async (entries, directory) => {
2102
+ const writeJavascriptCoverage = async (entries, directory, threshold, include) => {
2103
2103
  const rpc = create$1({
2104
2104
  commandMap: {},
2105
2105
  path: fileURLToPath(getCoverageWorkerUrl()),
2106
2106
  stdio: 'inherit'
2107
2107
  });
2108
2108
  try {
2109
- await rpc.invoke(WriteJavascriptCoverage, entries, directory);
2109
+ await rpc.invoke(WriteJavascriptCoverage, entries, directory, threshold, include);
2110
2110
  } finally {
2111
2111
  await rpc.dispose();
2112
2112
  }
2113
2113
  };
2114
2114
 
2115
+ const trackPromise = (promises, promise, onError) => {
2116
+ let trackedPromise = Promise.resolve();
2117
+ trackedPromise = (async () => {
2118
+ try {
2119
+ await promise;
2120
+ } catch (error) {
2121
+ await onError(error);
2122
+ } finally {
2123
+ promises.delete(trackedPromise);
2124
+ }
2125
+ })();
2126
+ promises.add(trackedPromise);
2127
+ };
2128
+ const startWorkerJavascriptCoverage = async (page, targetScript) => {
2129
+ const session = await page.context().newCDPSession(page);
2130
+ const sessions = new Set();
2131
+ const tasks = new Set();
2132
+ const entries = new Map();
2133
+ const pendingEntries = new Set();
2134
+ const timers = new Map();
2135
+ const pendingCommands = new Map();
2136
+ let commandId = 0;
2137
+ let collectionError;
2138
+ let stopped = false;
2139
+ const recordCollectionError = error => {
2140
+ const message = error instanceof Error ? error.message : String(error);
2141
+ if (!message.includes('No session with given id') && !message.includes('Target closed')) {
2142
+ collectionError = error instanceof Error ? error : new Error(message);
2143
+ }
2144
+ };
2145
+ const getCollectionError = () => collectionError;
2146
+ const onReceivedMessage = ({
2147
+ message
2148
+ }) => {
2149
+ const response = JSON.parse(message);
2150
+ if (response.id === undefined) {
2151
+ return;
2152
+ }
2153
+ const pending = pendingCommands.get(response.id);
2154
+ if (!pending) {
2155
+ return;
2156
+ }
2157
+ pendingCommands.delete(response.id);
2158
+ if (response.error) {
2159
+ pending.reject(new Error(response.error.message));
2160
+ } else {
2161
+ pending.resolve(response.result);
2162
+ }
2163
+ };
2164
+ const sendToTarget = (sessionId, method, params = {}) => {
2165
+ const id = ++commandId;
2166
+ const command = new Promise((resolve, reject) => {
2167
+ pendingCommands.set(id, {
2168
+ reject,
2169
+ resolve
2170
+ });
2171
+ });
2172
+ void session.send('Target.sendMessageToTarget', {
2173
+ message: JSON.stringify({
2174
+ id,
2175
+ method,
2176
+ params
2177
+ }),
2178
+ sessionId
2179
+ }).catch(error => {
2180
+ const message = error instanceof Error ? error.message : String(error);
2181
+ const pending = pendingCommands.get(id);
2182
+ pendingCommands.delete(id);
2183
+ pending?.reject(new Error(message));
2184
+ });
2185
+ return command;
2186
+ };
2187
+ const handleAttachedTarget = async ({
2188
+ sessionId,
2189
+ targetInfo
2190
+ }) => {
2191
+ const matches = targetInfo.type === 'worker' && targetInfo.url.includes(targetScript);
2192
+ if (matches) {
2193
+ sessions.add(sessionId);
2194
+ await sendToTarget(sessionId, 'Debugger.enable');
2195
+ await sendToTarget(sessionId, 'Profiler.enable');
2196
+ await sendToTarget(sessionId, 'Profiler.startPreciseCoverage', {
2197
+ callCount: true,
2198
+ detailed: true
2199
+ });
2200
+ const timer = setInterval(() => {
2201
+ trackPromise(pendingEntries, readTargetCoverage(sendToTarget, sessionId, entries), error => {
2202
+ recordCollectionError(error);
2203
+ });
2204
+ }, 50);
2205
+ timers.set(sessionId, timer);
2206
+ }
2207
+ await sendToTarget(sessionId, 'Runtime.runIfWaitingForDebugger');
2208
+ if (matches) {
2209
+ await readTargetCoverage(sendToTarget, sessionId, entries);
2210
+ }
2211
+ };
2212
+ const onAttached = target => {
2213
+ trackPromise(tasks, handleAttachedTarget(target), async error => {
2214
+ recordCollectionError(error);
2215
+ try {
2216
+ await sendToTarget(target.sessionId, 'Runtime.runIfWaitingForDebugger');
2217
+ } catch {
2218
+ // The target may have closed before it could be resumed.
2219
+ }
2220
+ });
2221
+ };
2222
+ const onDetached = ({
2223
+ sessionId
2224
+ }) => {
2225
+ if (!sessions.has(sessionId)) {
2226
+ return;
2227
+ }
2228
+ sessions.delete(sessionId);
2229
+ const timer = timers.get(sessionId);
2230
+ if (timer) {
2231
+ clearInterval(timer);
2232
+ timers.delete(sessionId);
2233
+ }
2234
+ };
2235
+ session.on('Target.attachedToTarget', onAttached);
2236
+ session.on('Target.detachedFromTarget', onDetached);
2237
+ session.on('Target.receivedMessageFromTarget', onReceivedMessage);
2238
+ await session.send('Target.setAutoAttach', {
2239
+ autoAttach: true,
2240
+ flatten: false,
2241
+ waitForDebuggerOnStart: true
2242
+ });
2243
+ return {
2244
+ async stop() {
2245
+ if (stopped) {
2246
+ return entries.values().toArray();
2247
+ }
2248
+ stopped = true;
2249
+ session.off('Target.attachedToTarget', onAttached);
2250
+ session.off('Target.detachedFromTarget', onDetached);
2251
+ try {
2252
+ for (const timer of timers.values()) {
2253
+ clearInterval(timer);
2254
+ }
2255
+ timers.clear();
2256
+ await session.send('Target.setAutoAttach', {
2257
+ autoAttach: false,
2258
+ flatten: false,
2259
+ waitForDebuggerOnStart: false
2260
+ });
2261
+ while (tasks.size > 0) {
2262
+ await Promise.all(tasks);
2263
+ }
2264
+ await Promise.all(pendingEntries);
2265
+ const firstCollectionError = getCollectionError();
2266
+ if (firstCollectionError) {
2267
+ throw new Error(firstCollectionError.message, {
2268
+ cause: firstCollectionError
2269
+ });
2270
+ }
2271
+ for (const sessionId of sessions) {
2272
+ trackPromise(pendingEntries, readTargetCoverage(sendToTarget, sessionId, entries), error => {
2273
+ recordCollectionError(error);
2274
+ });
2275
+ }
2276
+ await Promise.all(pendingEntries);
2277
+ const finalCollectionError = getCollectionError();
2278
+ if (finalCollectionError) {
2279
+ throw new Error(finalCollectionError.message, {
2280
+ cause: finalCollectionError
2281
+ });
2282
+ }
2283
+ for (const sessionId of sessions) {
2284
+ try {
2285
+ await sendToTarget(sessionId, 'Profiler.stopPreciseCoverage');
2286
+ } catch {
2287
+ // A target can exit after its last coverage snapshot.
2288
+ }
2289
+ }
2290
+ const result = entries.values().toArray();
2291
+ if (result.length === 0) {
2292
+ throw new Error(`[test-with-playwright] no JavaScript coverage was collected for worker target ${targetScript}`);
2293
+ }
2294
+ return result;
2295
+ } finally {
2296
+ session.off('Target.receivedMessageFromTarget', onReceivedMessage);
2297
+ await session.detach();
2298
+ }
2299
+ }
2300
+ };
2301
+ };
2302
+ const readTargetCoverage = async (sendToTarget, sessionId, entries) => {
2303
+ try {
2304
+ const {
2305
+ result
2306
+ } = await sendToTarget(sessionId, 'Profiler.takePreciseCoverage');
2307
+ for (const script of result) {
2308
+ if (!script.url) {
2309
+ continue;
2310
+ }
2311
+ const {
2312
+ scriptSource
2313
+ } = await sendToTarget(sessionId, 'Debugger.getScriptSource', {
2314
+ scriptId: script.scriptId
2315
+ });
2316
+ const key = `${sessionId}:${script.scriptId}`;
2317
+ const previous = entries.get(key);
2318
+ const functions = script.functions.map((fn, index) => {
2319
+ const previousFunction = previous?.functions[index];
2320
+ return {
2321
+ functionName: fn.functionName,
2322
+ isBlockCoverage: fn.isBlockCoverage,
2323
+ ranges: fn.ranges.map((range, rangeIndex) => {
2324
+ const previousRange = previousFunction?.ranges[rangeIndex];
2325
+ return {
2326
+ count: range.count + (previousRange?.count ?? 0),
2327
+ endOffset: range.endOffset,
2328
+ startOffset: range.startOffset
2329
+ };
2330
+ })
2331
+ };
2332
+ });
2333
+ entries.set(key, {
2334
+ functions,
2335
+ scriptId: script.scriptId,
2336
+ source: previous?.source ?? scriptSource,
2337
+ url: script.url
2338
+ });
2339
+ }
2340
+ } catch (error) {
2341
+ const message = error instanceof Error ? error.message : String(error);
2342
+ if (!message.includes('No session with given id') && !message.includes('Target closed')) {
2343
+ throw error;
2344
+ }
2345
+ }
2346
+ };
2347
+
2115
2348
  const runWithJavascriptCoverage = async ({
2116
2349
  coverage,
2350
+ coverageInclude,
2351
+ coverageTarget,
2352
+ coverageThreshold,
2117
2353
  cwd,
2118
2354
  page,
2119
2355
  run
2120
2356
  }) => {
2121
- if (!coverage) {
2357
+ if (!coverage && !coverageTarget) {
2122
2358
  await run();
2123
2359
  return;
2124
2360
  }
2125
- await page.coverage.startJSCoverage({
2126
- resetOnNavigation: false
2127
- });
2361
+ const workerCoverage = coverageTarget ? await startWorkerJavascriptCoverage(page, coverageTarget) : undefined;
2362
+ let pageCoverageStarted = false;
2128
2363
  try {
2364
+ if (coverage) {
2365
+ await page.coverage.startJSCoverage({
2366
+ resetOnNavigation: false
2367
+ });
2368
+ pageCoverageStarted = true;
2369
+ }
2129
2370
  await run();
2130
2371
  } finally {
2131
- const entries = await page.coverage.stopJSCoverage();
2132
- await writeJavascriptCoverage(entries, join(cwd, 'coverage'));
2372
+ let workerEntries;
2373
+ if (pageCoverageStarted) {
2374
+ try {
2375
+ const entries = await page.coverage.stopJSCoverage();
2376
+ await writeJavascriptCoverage(entries, join(cwd, 'coverage'));
2377
+ } finally {
2378
+ workerEntries = await workerCoverage?.stop();
2379
+ }
2380
+ } else if (workerCoverage) {
2381
+ workerEntries = await workerCoverage.stop();
2382
+ }
2383
+ if (workerEntries) {
2384
+ await writeJavascriptCoverage(workerEntries, join(cwd, 'coverage', 'worker'), coverageThreshold, coverageInclude);
2385
+ }
2133
2386
  }
2134
2387
  };
2135
2388
 
@@ -2628,6 +2881,17 @@ const tearDownTests = async ({
2628
2881
  }
2629
2882
  };
2630
2883
 
2884
+ const assertCoverageOptions = (coverageTarget, coverageInclude, coverageThreshold) => {
2885
+ if (coverageTarget !== undefined) {
2886
+ string(coverageTarget);
2887
+ }
2888
+ if (coverageInclude !== undefined) {
2889
+ string(coverageInclude);
2890
+ }
2891
+ if (coverageThreshold !== undefined) {
2892
+ number(coverageThreshold);
2893
+ }
2894
+ };
2631
2895
  /**
2632
2896
  * @param {string} extensionPath
2633
2897
  * @param {string} testPath
@@ -2642,7 +2906,7 @@ const tearDownTests = async ({
2642
2906
  * @param {boolean} coverage
2643
2907
  * @param {boolean} traceRendererWorker
2644
2908
  */
2645
- const runAllTests = async (extensionPath, testPath, cwd, browser, headless, timeout, runtimeOptions, traceFocus, filter, reusePage, svgScreenshotOptions, coverage, traceRendererWorker) => {
2909
+ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, timeout, runtimeOptions, traceFocus, filter, reusePage, svgScreenshotOptions, coverage, coverageInclude, coverageTarget, coverageThreshold, traceRendererWorker) => {
2646
2910
  string(extensionPath);
2647
2911
  string(testPath);
2648
2912
  string(cwd);
@@ -2652,6 +2916,7 @@ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, time
2652
2916
  object(runtimeOptions);
2653
2917
  boolean(reusePage);
2654
2918
  boolean(coverage);
2919
+ assertCoverageOptions(coverageTarget, coverageInclude, coverageThreshold);
2655
2920
  boolean(traceRendererWorker);
2656
2921
  if (svgScreenshotOptions !== undefined) {
2657
2922
  object(svgScreenshotOptions);
@@ -2683,6 +2948,15 @@ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, time
2683
2948
  });
2684
2949
  await runWithJavascriptCoverage({
2685
2950
  coverage,
2951
+ ...(coverageTarget !== undefined && {
2952
+ coverageTarget
2953
+ }),
2954
+ ...(coverageInclude !== undefined && {
2955
+ coverageInclude
2956
+ }),
2957
+ ...(coverageThreshold !== undefined && {
2958
+ coverageThreshold
2959
+ }),
2686
2960
  cwd,
2687
2961
  page: electron.page,
2688
2962
  run: async () => {
@@ -2728,6 +3002,15 @@ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, time
2728
3002
  if (reusePage) {
2729
3003
  await runWithJavascriptCoverage({
2730
3004
  coverage,
3005
+ ...(coverageTarget !== undefined && {
3006
+ coverageTarget
3007
+ }),
3008
+ ...(coverageInclude !== undefined && {
3009
+ coverageInclude
3010
+ }),
3011
+ ...(coverageThreshold !== undefined && {
3012
+ coverageThreshold
3013
+ }),
2731
3014
  cwd,
2732
3015
  page,
2733
3016
  run: async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/test-with-playwright-worker",
3
- "version": "22.39.2",
3
+ "version": "22.39.4",
4
4
  "description": "Worker package for test-with-playwright",
5
5
  "repository": {
6
6
  "type": "git",