@empiricalrun/playwright-utils 0.58.6 → 0.60.0

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 +1 @@
1
- {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAUnC,YAAY,EACV,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,6CAA6C,CAAC;AA+JrD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAuBtB"}
1
+ {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAUnC,YAAY,EACV,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,6CAA6C,CAAC;AAgUrD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAyBtB"}
@@ -15,6 +15,24 @@ function getSuiteParallelMode(suite) {
15
15
  ? mode
16
16
  : "none";
17
17
  }
18
+ /**
19
+ * The project's per-project `workers` cap, if it declares one.
20
+ *
21
+ * The public `FullProject` deliberately omits `workers`, so this reads the
22
+ * internal `FullProjectInternal` that Playwright attaches to every project
23
+ * suite (`buildProjectSuite` in `runner/loadUtils.ts` — the same object
24
+ * `suite.project()` resolves through, so it is present wherever projects
25
+ * are). Only explicit per-project caps surface here: the global `workers`
26
+ * setting resolves per machine (a `'50%'` default is a different number on
27
+ * the listing container than on a shard) and is not a per-project signal.
28
+ */
29
+ function getProjectWorkerCap(suite) {
30
+ const workers = suite
31
+ ._fullProject?.workers;
32
+ return typeof workers === "number" && Number.isFinite(workers) && workers >= 1
33
+ ? workers
34
+ : undefined;
35
+ }
18
36
  function toPosixPath(value) {
19
37
  return value.split(path_1.default.sep).join("/");
20
38
  }
@@ -41,7 +59,7 @@ function stableGroupId(input) {
41
59
  input.column,
42
60
  ]))
43
61
  .digest("hex");
44
- return `playwright_serial:${hash}`;
62
+ return `${input.kind}:${hash}`;
45
63
  }
46
64
  function collectDirectTestCases(rootDir, suite, state) {
47
65
  return suite.tests.map((test) => ({
@@ -53,11 +71,7 @@ function collectDirectTestCases(rootDir, suite, state) {
53
71
  tags: normalizeTags(test.tags),
54
72
  }));
55
73
  }
56
- function collectOutermostSerialGroup(rootDir, suite, state) {
57
- if (state.serialAncestor || getSuiteParallelMode(suite) !== "serial") {
58
- return null;
59
- }
60
- const tests = suite.allTests();
74
+ function buildDependencyGroup(rootDir, suite, state, kind, tests) {
61
75
  if (tests.length < 2) {
62
76
  return null;
63
77
  }
@@ -71,13 +85,14 @@ function collectOutermostSerialGroup(rootDir, suite, state) {
71
85
  const location = suite.location ?? firstTest.location;
72
86
  return {
73
87
  id: stableGroupId({
88
+ kind,
74
89
  projectName,
75
90
  filePath,
76
91
  titlePath,
77
92
  line: location.line,
78
93
  column: location.column,
79
94
  }),
80
- kind: "playwright_serial",
95
+ kind,
81
96
  file_path: filePath,
82
97
  title_path: titlePath,
83
98
  playwright_project: projectName,
@@ -87,14 +102,95 @@ function collectOutermostSerialGroup(rootDir, suite, state) {
87
102
  })),
88
103
  };
89
104
  }
105
+ function collectOutermostSerialGroup(rootDir, suite, state) {
106
+ if (state.serialAncestor || getSuiteParallelMode(suite) !== "serial") {
107
+ return null;
108
+ }
109
+ return buildDependencyGroup(rootDir, suite, state, "playwright_serial", suite.allTests());
110
+ }
111
+ /**
112
+ * Tests under this suite that Playwright would place in its sequential
113
+ * ("general") group: everything except subtrees that opt back into
114
+ * parallelism. Order is declaration order, which is the order the group
115
+ * executes in.
116
+ */
117
+ function sequentialTests(suite) {
118
+ const tests = [...suite.tests];
119
+ for (const child of suite.suites) {
120
+ if (getSuiteParallelMode(child) !== "parallel") {
121
+ tests.push(...sequentialTests(child));
122
+ }
123
+ }
124
+ return tests;
125
+ }
126
+ /**
127
+ * The `playwright_default` counterpart of the serial collector: the outermost
128
+ * scope whose tests Playwright schedules as one ordered worker group without
129
+ * serial semantics.
130
+ *
131
+ * Two shapes qualify (mirroring `createTestGroups` in Playwright's
132
+ * `runner/testGroups.ts`):
133
+ *
134
+ * - a `test.describe.configure({ mode: "default" })` suite — grouped whole,
135
+ * including any nested parallel subtree, because Playwright keys the group
136
+ * on the outermost sequential suite;
137
+ * - a file with no explicit mode in a non-fullyParallel project — Playwright's
138
+ * per-file "general" group, which excludes subtrees that opt back into
139
+ * `parallel`.
140
+ */
141
+ function collectOutermostDefaultGroup(rootDir, suite, state) {
142
+ if (state.sequentialAncestor) {
143
+ return null;
144
+ }
145
+ const mode = getSuiteParallelMode(suite);
146
+ if (mode === "default") {
147
+ return buildDependencyGroup(rootDir, suite, state, "playwright_default", suite.allTests());
148
+ }
149
+ if (suite.type === "file" && !state.insideParallel && mode === "none") {
150
+ return buildDependencyGroup(rootDir, suite, state, "playwright_default", sequentialTests(suite));
151
+ }
152
+ return null;
153
+ }
90
154
  function walkSuite(rootDir, suite, state, testCases, dependencyGroups) {
91
155
  const isDescribe = suite.type === "describe";
92
156
  const suites = isDescribe ? [...state.suites, suite.title] : state.suites;
93
157
  const nextState = { ...state, suites };
158
+ const mode = getSuiteParallelMode(suite);
159
+ // A per-project `workers` cap constrains the whole project: the dispatcher
160
+ // enforces it per process (`_workerLimitPerProjectId`), so the guarantee
161
+ // only survives sharding if every test of the project lands on one shard.
162
+ // Emitted alongside any inner groups — it is the outermost scope, so it
163
+ // wins for scheduling and keeps the inner ones whole for free.
164
+ if (suite.type === "project") {
165
+ const workerCap = getProjectWorkerCap(suite);
166
+ if (workerCap !== undefined) {
167
+ const capGroup = buildDependencyGroup(rootDir, suite, nextState, "playwright_worker_cap", suite.allTests());
168
+ if (capGroup) {
169
+ dependencyGroups.push({ ...capGroup, worker_limit: workerCap });
170
+ }
171
+ }
172
+ }
173
+ // A serial group is emitted even inside a default scope: the outer default
174
+ // group wins for scheduling, but serial membership carries extra semantics
175
+ // (failure cascade, re-expansion) its consumers still need.
94
176
  const serialGroup = collectOutermostSerialGroup(rootDir, suite, nextState);
95
177
  if (serialGroup) {
96
178
  dependencyGroups.push(serialGroup);
97
179
  nextState.serialAncestor = suite;
180
+ if (!nextState.sequentialAncestor) {
181
+ nextState.sequentialAncestor = suite;
182
+ }
183
+ }
184
+ const defaultGroup = collectOutermostDefaultGroup(rootDir, suite, nextState);
185
+ if (defaultGroup) {
186
+ dependencyGroups.push(defaultGroup);
187
+ nextState.sequentialAncestor = suite;
188
+ }
189
+ // fullyParallel (project suite) or an explicit `mode: "parallel"` — every
190
+ // descendant is independently schedulable unless a sequential suite below
191
+ // says otherwise.
192
+ if (mode === "parallel") {
193
+ nextState.insideParallel = true;
98
194
  }
99
195
  testCases.push(...collectDirectTestCases(rootDir, suite, nextState));
100
196
  for (const child of suite.suites) {
@@ -112,6 +208,15 @@ function walkSuite(rootDir, suite, state, testCases, dependencyGroups) {
112
208
  : "";
113
209
  childState.suites = [];
114
210
  childState.serialAncestor = null;
211
+ childState.sequentialAncestor = null;
212
+ }
213
+ else if (childState.sequentialAncestor &&
214
+ getSuiteParallelMode(childState.sequentialAncestor) === "none" &&
215
+ getSuiteParallelMode(child) === "parallel") {
216
+ // The scope was a plain file's "general" group, which excludes subtrees
217
+ // that opt back into parallel — so below this child, sequential suites
218
+ // start their own groups again.
219
+ childState.sequentialAncestor = null;
115
220
  }
116
221
  walkSuite(rootDir, child, childState, testCases, dependencyGroups);
117
222
  }
@@ -125,6 +230,8 @@ function collectTestCaseSyncManifest(config, rootSuite) {
125
230
  filePath: "",
126
231
  suites: [],
127
232
  serialAncestor: null,
233
+ sequentialAncestor: null,
234
+ insideParallel: false,
128
235
  }, testCases, dependencyGroups);
129
236
  return {
130
237
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.58.6",
3
+ "version": "0.60.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -41,7 +41,7 @@
41
41
  "openapi-typescript": "7.6.1",
42
42
  "playwright-core": "1.62.1",
43
43
  "serve-handler": "^6.1.6",
44
- "@empiricalrun/shared-types": "0.15.4"
44
+ "@empiricalrun/shared-types": "0.17.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "@babel/code-frame": "^7.24.7",