@specific.dev/spectest 0.90.2 → 0.90.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.
@@ -22,10 +22,10 @@ export declare function foldBrowserCoverage(service: string, files: FileCounts):
22
22
  /** Harvest every live session, so a capture sees the counters of code
23
23
  * that ran since the last browser op. */
24
24
  export declare function harvestAllBrowserCoverage(): Promise<void>;
25
- /** One lcov document per service that has any browser coverage — and
26
- * the totals are cleared, so the next capture holds only what runs
27
- * from now on. Module memory forks with the environment, so a forked
28
- * child starts from its parent's last capture, empty. */
29
- export declare function takeBrowserCoverageReports(): Map<string, string>;
25
+ /** Take one service's lcov report and clear only its totals. Other
26
+ * services retain their coverage until their own capture. Each capture
27
+ * is a delta; after every service is captured, a forked child inherits
28
+ * empty totals from its parent. */
29
+ export declare function takeBrowserCoverageReport(service: string): string;
30
30
  /** The current totals, without clearing them (tests). */
31
31
  export declare function browserCoverageReports(): Map<string, string>;
@@ -12,8 +12,8 @@
12
12
  //
13
13
  // Harvest runs before every browser step (a click that navigates
14
14
  // cross-site replaces the renderer, and the old counters with it) and at
15
- // every coverage capture. `browserCoverageReports` renders the totals as
16
- // one lcov per service for the `case-coverage` bundle.
15
+ // every coverage capture. `takeBrowserCoverageReport` renders and clears
16
+ // one service's totals for the `case-coverage` bundle.
17
17
  //
18
18
  // Configured by the daemon at /load with the host → service table; a
19
19
  // project with no `coverage: { browser: true }` service never attaches
@@ -196,18 +196,15 @@ function parseDataUrl(ref) {
196
196
  export async function harvestAllBrowserCoverage() {
197
197
  await Promise.all([...LIVE_COLLECTORS].map((c) => harvestBrowserCoverage(c.cdp)));
198
198
  }
199
- /** One lcov document per service that has any browser coverage — and
200
- * the totals are cleared, so the next capture holds only what runs
201
- * from now on. Module memory forks with the environment, so a forked
202
- * child starts from its parent's last capture, empty. */
203
- export function takeBrowserCoverageReports() {
204
- const out = new Map();
205
- for (const [service, files] of TOTALS) {
206
- if (files.size > 0)
207
- out.set(service, lcovDocument(files));
208
- }
209
- TOTALS.clear();
210
- return out;
199
+ /** Take one service's lcov report and clear only its totals. Other
200
+ * services retain their coverage until their own capture. Each capture
201
+ * is a delta; after every service is captured, a forked child inherits
202
+ * empty totals from its parent. */
203
+ export function takeBrowserCoverageReport(service) {
204
+ const files = TOTALS.get(service);
205
+ const report = files ? lcovDocument(files) : "TN:\n";
206
+ TOTALS.delete(service);
207
+ return report;
211
208
  }
212
209
  /** The current totals, without clearing them (tests). */
213
210
  export function browserCoverageReports() {
@@ -294,7 +294,7 @@ export declare function collectNodeReports(dir: string): Promise<NodeReportStats
294
294
  * loads from this service's origins, maps it back to source through the
295
295
  * served source map, and writes one lcov per capture (`browser.lcov`)
296
296
  * holding what ran since the previous capture: every live session is
297
- * harvested first, and the totals are taken and cleared. Nothing ran ⇒
297
+ * harvested first, and only this service's totals are taken and cleared. Nothing ran ⇒
298
298
  * an lcov with no records, which is a report saying so.
299
299
  */
300
300
  export declare function browser(): CoverageAdapter;
package/dist/coverage.js CHANGED
@@ -27,7 +27,7 @@
27
27
  // import * as coverage from "@specific.dev/spectest/coverage";
28
28
  // coverage: { adapters: [coverage.node(), coverage.browser()] }
29
29
  import { createHash } from "node:crypto";
30
- import { harvestAllBrowserCoverage, takeBrowserCoverageReports } from "./browser-coverage.js";
30
+ import { harvestAllBrowserCoverage, takeBrowserCoverageReport } from "./browser-coverage.js";
31
31
  import { COVERAGE_CONTAINER_DIR } from "./harness/coverage.js";
32
32
  export { COVERAGE_CONTAINER_DIR };
33
33
  /** Where `node()` mounts its hook inside the container. */
@@ -810,7 +810,7 @@ export async function collectNodeReports(dir) {
810
810
  * loads from this service's origins, maps it back to source through the
811
811
  * served source map, and writes one lcov per capture (`browser.lcov`)
812
812
  * holding what ran since the previous capture: every live session is
813
- * harvested first, and the totals are taken and cleared. Nothing ran ⇒
813
+ * harvested first, and only this service's totals are taken and cleared. Nothing ran ⇒
814
814
  * an lcov with no records, which is a report saying so.
815
815
  */
816
816
  export function browser() {
@@ -822,7 +822,7 @@ export function browser() {
822
822
  },
823
823
  async capture(ctx) {
824
824
  await harvestAllBrowserCoverage();
825
- const lcov = takeBrowserCoverageReports().get(ctx.service) ?? "TN:\n";
825
+ const lcov = takeBrowserCoverageReport(ctx.service);
826
826
  await ctx.writeReport("browser.lcov", lcov);
827
827
  },
828
828
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.90.2",
3
+ "version": "0.90.3",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,8 +12,8 @@
12
12
  //
13
13
  // Harvest runs before every browser step (a click that navigates
14
14
  // cross-site replaces the renderer, and the old counters with it) and at
15
- // every coverage capture. `browserCoverageReports` renders the totals as
16
- // one lcov per service for the `case-coverage` bundle.
15
+ // every coverage capture. `takeBrowserCoverageReport` renders and clears
16
+ // one service's totals for the `case-coverage` bundle.
17
17
  //
18
18
  // Configured by the daemon at /load with the host → service table; a
19
19
  // project with no `coverage: { browser: true }` service never attaches
@@ -220,17 +220,15 @@ export async function harvestAllBrowserCoverage(): Promise<void> {
220
220
  await Promise.all([...LIVE_COLLECTORS].map((c) => harvestBrowserCoverage(c.cdp)));
221
221
  }
222
222
 
223
- /** One lcov document per service that has any browser coverage — and
224
- * the totals are cleared, so the next capture holds only what runs
225
- * from now on. Module memory forks with the environment, so a forked
226
- * child starts from its parent's last capture, empty. */
227
- export function takeBrowserCoverageReports(): Map<string, string> {
228
- const out = new Map<string, string>();
229
- for (const [service, files] of TOTALS) {
230
- if (files.size > 0) out.set(service, lcovDocument(files));
231
- }
232
- TOTALS.clear();
233
- return out;
223
+ /** Take one service's lcov report and clear only its totals. Other
224
+ * services retain their coverage until their own capture. Each capture
225
+ * is a delta; after every service is captured, a forked child inherits
226
+ * empty totals from its parent. */
227
+ export function takeBrowserCoverageReport(service: string): string {
228
+ const files = TOTALS.get(service);
229
+ const report = files ? lcovDocument(files) : "TN:\n";
230
+ TOTALS.delete(service);
231
+ return report;
234
232
  }
235
233
 
236
234
  /** The current totals, without clearing them (tests). */
@@ -135,6 +135,56 @@ describe("units", () => {
135
135
  });
136
136
 
137
137
  describe("browserCoverage", () => {
138
+ for (const order of ["dashboard first", "admin first", "concurrent"] as const) {
139
+ test(`services retain their own coverage and deltas: ${order}`, async () => {
140
+ const reports = new Map<string, string>();
141
+ const contexts = ["dashboard", "admin"].map((service): CoverageCaptureContext => ({
142
+ ...fakeCtx("unused"),
143
+ service,
144
+ async writeReport(name, content) {
145
+ expect(name).toBe("browser.lcov");
146
+ reports.set(service, content);
147
+ },
148
+ }));
149
+ if (order === "admin first") contexts.reverse();
150
+ const capture = async () => {
151
+ if (order === "concurrent") {
152
+ await Promise.all(contexts.map((ctx) => browser().capture!(ctx)));
153
+ } else {
154
+ for (const ctx of contexts) await browser().capture!(ctx);
155
+ }
156
+ };
157
+ foldBrowserCoverage("dashboard", new Map([["src/dashboard.ts", new Map([[1, 2]])]]));
158
+ foldBrowserCoverage("admin", new Map([["src/admin.ts", new Map([[3, 4]])]]));
159
+ await capture();
160
+ expect(reports.get("dashboard")).toBe("TN:\nSF:src/dashboard.ts\nDA:1,2\nLF:1\nLH:1\nend_of_record\n");
161
+ expect(reports.get("admin")).toBe("TN:\nSF:src/admin.ts\nDA:3,4\nLF:1\nLH:1\nend_of_record\n");
162
+
163
+ await capture();
164
+ expect(reports.get("dashboard")).toBe("TN:\n");
165
+ expect(reports.get("admin")).toBe("TN:\n");
166
+
167
+ foldBrowserCoverage("admin", new Map([["src/admin.ts", new Map([[3, 1]])]]));
168
+ await capture();
169
+ expect(reports.get("dashboard")).toBe("TN:\n");
170
+ expect(reports.get("admin")).toBe("TN:\nSF:src/admin.ts\nDA:3,1\nLF:1\nLH:1\nend_of_record\n");
171
+ });
172
+ }
173
+
174
+ test("capturing a service with no coverage preserves other services' totals", async () => {
175
+ const reports = new Map<string, string>();
176
+ foldBrowserCoverage("dashboard", new Map([["src/dashboard.ts", new Map([[1, 2]])]]));
177
+ for (const service of ["admin", "dashboard"]) {
178
+ await browser().capture!({
179
+ ...fakeCtx("unused"),
180
+ service,
181
+ async writeReport(_name, content) { reports.set(service, content); },
182
+ });
183
+ }
184
+ expect(reports.get("admin")).toBe("TN:\n");
185
+ expect(reports.get("dashboard")).toBe("TN:\nSF:src/dashboard.ts\nDA:1,2\nLF:1\nLH:1\nend_of_record\n");
186
+ });
187
+
138
188
  test("writes an empty lcov when nothing ran for the service", async () => {
139
189
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-cov-"));
140
190
  await browser().capture!(fakeCtx(dir));
package/src/coverage.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  import { createHash } from "node:crypto";
31
31
 
32
32
  import type { ServiceConfig } from "./index.js";
33
- import { harvestAllBrowserCoverage, takeBrowserCoverageReports } from "./browser-coverage.js";
33
+ import { harvestAllBrowserCoverage, takeBrowserCoverageReport } from "./browser-coverage.js";
34
34
 
35
35
  import { COVERAGE_CONTAINER_DIR, type CoverageReportsMode } from "./harness/coverage.js";
36
36
  export { COVERAGE_CONTAINER_DIR, type CoverageReportsMode };
@@ -969,7 +969,7 @@ export async function collectNodeReports(dir: string): Promise<NodeReportStats>
969
969
  * loads from this service's origins, maps it back to source through the
970
970
  * served source map, and writes one lcov per capture (`browser.lcov`)
971
971
  * holding what ran since the previous capture: every live session is
972
- * harvested first, and the totals are taken and cleared. Nothing ran ⇒
972
+ * harvested first, and only this service's totals are taken and cleared. Nothing ran ⇒
973
973
  * an lcov with no records, which is a report saying so.
974
974
  */
975
975
  export function browser(): CoverageAdapter {
@@ -981,7 +981,7 @@ export function browser(): CoverageAdapter {
981
981
  },
982
982
  async capture(ctx) {
983
983
  await harvestAllBrowserCoverage();
984
- const lcov = takeBrowserCoverageReports().get(ctx.service) ?? "TN:\n";
984
+ const lcov = takeBrowserCoverageReport(ctx.service);
985
985
  await ctx.writeReport("browser.lcov", lcov);
986
986
  },
987
987
  };