stably 4.8.9 → 4.10.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.
Files changed (47) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/stably-plugin-cli/.claude-plugin/plugin.json +5 -0
  3. package/dist/stably-plugin-cli/skills/bash-commands/SKILL.md +65 -0
  4. package/dist/stably-plugin-cli/skills/browser-interaction-guide/SKILL.md +144 -0
  5. package/dist/stably-plugin-cli/skills/bulk-test-handling/SKILL.md +104 -0
  6. package/dist/stably-plugin-cli/skills/debugging-test-failures/SKILL.md +146 -0
  7. package/dist/{stably-plugin → stably-plugin-cli}/skills/playwright-best-practices/SKILL.md +11 -5
  8. package/dist/stably-plugin-cli/skills/playwright-config-auth/SKILL.md +217 -0
  9. package/dist/stably-plugin-cli/skills/stably-sdk-reference/SKILL.md +307 -0
  10. package/dist/stably-plugin-cli/skills/test-creation-workflow/SKILL.md +311 -0
  11. package/package.json +4 -1
  12. package/dist/stably-plugin/.claude-plugin/plugin.json +0 -5
  13. package/dist/stably-plugin/skills/playwright-best-practices/references/accessibility.md +0 -359
  14. package/dist/stably-plugin/skills/playwright-best-practices/references/annotations.md +0 -526
  15. package/dist/stably-plugin/skills/playwright-best-practices/references/assertions-waiting.md +0 -361
  16. package/dist/stably-plugin/skills/playwright-best-practices/references/browser-apis.md +0 -391
  17. package/dist/stably-plugin/skills/playwright-best-practices/references/browser-extensions.md +0 -506
  18. package/dist/stably-plugin/skills/playwright-best-practices/references/canvas-webgl.md +0 -493
  19. package/dist/stably-plugin/skills/playwright-best-practices/references/ci-cd.md +0 -407
  20. package/dist/stably-plugin/skills/playwright-best-practices/references/clock-mocking.md +0 -364
  21. package/dist/stably-plugin/skills/playwright-best-practices/references/component-testing.md +0 -500
  22. package/dist/stably-plugin/skills/playwright-best-practices/references/console-errors.md +0 -420
  23. package/dist/stably-plugin/skills/playwright-best-practices/references/debugging.md +0 -491
  24. package/dist/stably-plugin/skills/playwright-best-practices/references/electron.md +0 -509
  25. package/dist/stably-plugin/skills/playwright-best-practices/references/error-testing.md +0 -360
  26. package/dist/stably-plugin/skills/playwright-best-practices/references/file-operations.md +0 -375
  27. package/dist/stably-plugin/skills/playwright-best-practices/references/fixtures-hooks.md +0 -417
  28. package/dist/stably-plugin/skills/playwright-best-practices/references/flaky-tests.md +0 -494
  29. package/dist/stably-plugin/skills/playwright-best-practices/references/global-setup.md +0 -434
  30. package/dist/stably-plugin/skills/playwright-best-practices/references/i18n.md +0 -508
  31. package/dist/stably-plugin/skills/playwright-best-practices/references/iframes.md +0 -403
  32. package/dist/stably-plugin/skills/playwright-best-practices/references/locators.md +0 -242
  33. package/dist/stably-plugin/skills/playwright-best-practices/references/mobile-testing.md +0 -409
  34. package/dist/stably-plugin/skills/playwright-best-practices/references/multi-context.md +0 -288
  35. package/dist/stably-plugin/skills/playwright-best-practices/references/multi-user.md +0 -393
  36. package/dist/stably-plugin/skills/playwright-best-practices/references/network-advanced.md +0 -452
  37. package/dist/stably-plugin/skills/playwright-best-practices/references/page-object-model.md +0 -315
  38. package/dist/stably-plugin/skills/playwright-best-practices/references/performance-testing.md +0 -476
  39. package/dist/stably-plugin/skills/playwright-best-practices/references/performance.md +0 -453
  40. package/dist/stably-plugin/skills/playwright-best-practices/references/projects-dependencies.md +0 -456
  41. package/dist/stably-plugin/skills/playwright-best-practices/references/security-testing.md +0 -430
  42. package/dist/stably-plugin/skills/playwright-best-practices/references/service-workers.md +0 -504
  43. package/dist/stably-plugin/skills/playwright-best-practices/references/test-coverage.md +0 -495
  44. package/dist/stably-plugin/skills/playwright-best-practices/references/test-data.md +0 -492
  45. package/dist/stably-plugin/skills/playwright-best-practices/references/test-organization.md +0 -361
  46. package/dist/stably-plugin/skills/playwright-best-practices/references/third-party.md +0 -464
  47. package/dist/stably-plugin/skills/playwright-best-practices/references/websockets.md +0 -403
@@ -1,495 +0,0 @@
1
- # Test Coverage
2
-
3
- ## Table of Contents
4
-
5
- 1. [Coverage Setup](#coverage-setup)
6
- 2. [Collecting Coverage](#collecting-coverage)
7
- 3. [Coverage Reports](#coverage-reports)
8
- 4. [Coverage Thresholds](#coverage-thresholds)
9
- 5. [Advanced Patterns](#advanced-patterns)
10
- 6. [CI Integration](#ci-integration)
11
-
12
- ## Coverage Setup
13
-
14
- ### Install Dependencies
15
-
16
- ```bash
17
- # For V8 coverage (built into Playwright)
18
- # No additional dependencies needed
19
-
20
- # For Istanbul-based coverage (more features)
21
- npm install -D nyc @istanbuljs/nyc-config-typescript
22
- ```
23
-
24
- ### Basic Configuration
25
-
26
- ```typescript
27
- // playwright.config.ts
28
- import { defineConfig } from "@playwright/test";
29
-
30
- export default defineConfig({
31
- use: {
32
- // Enable coverage collection
33
- contextOptions: {
34
- // V8 coverage is automatic with the API below
35
- },
36
- },
37
- });
38
- ```
39
-
40
- ### V8 Coverage Fixture
41
-
42
- ```typescript
43
- // fixtures/coverage.ts
44
- import { test as base, expect } from "@playwright/test";
45
- import fs from "fs";
46
- import path from "path";
47
- import { randomUUID } from "crypto";
48
-
49
- export const test = base.extend<{}, { collectCoverage: void }>({
50
- collectCoverage: [
51
- async ({ browser }, use) => {
52
- // Start coverage for all pages
53
- const context = await browser.newContext();
54
- const page = await context.newPage();
55
-
56
- await page.coverage.startJSCoverage();
57
- await page.coverage.startCSSCoverage();
58
-
59
- await use();
60
-
61
- // Collect coverage
62
- const [jsCoverage, cssCoverage] = await Promise.all([
63
- page.coverage.stopJSCoverage(),
64
- page.coverage.stopCSSCoverage(),
65
- ]);
66
-
67
- // Save coverage data
68
- const coverageDir = "./coverage";
69
- if (!fs.existsSync(coverageDir)) {
70
- fs.mkdirSync(coverageDir, { recursive: true });
71
- }
72
-
73
- fs.writeFileSync(
74
- path.join(coverageDir, `coverage-${randomUUID()}.json`),
75
- JSON.stringify([...jsCoverage, ...cssCoverage]),
76
- );
77
-
78
- await context.close();
79
- },
80
- { scope: "worker", auto: true },
81
- ],
82
- });
83
- ```
84
-
85
- ## Collecting Coverage
86
-
87
- ### Per-Test Coverage
88
-
89
- ```typescript
90
- test("collect coverage for single test", async ({ page }) => {
91
- // Start coverage collection
92
- await page.coverage.startJSCoverage({
93
- resetOnNavigation: false,
94
- });
95
-
96
- // Run test
97
- await page.goto("/app");
98
- await page.getByRole("button", { name: "Submit" }).click();
99
- await expect(page.getByText("Success")).toBeVisible();
100
-
101
- // Stop and get coverage
102
- const coverage = await page.coverage.stopJSCoverage();
103
-
104
- // Filter to only your source files
105
- const appCoverage = coverage.filter((entry) => entry.url.includes("/src/"));
106
-
107
- console.log(`Covered ${appCoverage.length} source files`);
108
- });
109
- ```
110
-
111
- ### Coverage for Specific Files
112
-
113
- ```typescript
114
- test("track specific module coverage", async ({ page }) => {
115
- await page.coverage.startJSCoverage();
116
-
117
- await page.goto("/checkout");
118
- await page.getByRole("button", { name: "Pay" }).click();
119
-
120
- const coverage = await page.coverage.stopJSCoverage();
121
-
122
- // Find coverage for checkout module
123
- const checkoutCoverage = coverage.find((c) => c.url.includes("checkout.js"));
124
-
125
- if (checkoutCoverage) {
126
- const totalBytes = checkoutCoverage.text?.length || 0;
127
- const coveredBytes = checkoutCoverage.ranges.reduce(
128
- (sum, range) => sum + (range.end - range.start),
129
- 0,
130
- );
131
- const percentage = (coveredBytes / totalBytes) * 100;
132
-
133
- console.log(`Checkout module: ${percentage.toFixed(1)}% covered`);
134
- expect(percentage).toBeGreaterThan(80);
135
- }
136
- });
137
- ```
138
-
139
- ### CSS Coverage
140
-
141
- ```typescript
142
- test("collect CSS coverage", async ({ page }) => {
143
- await page.coverage.startCSSCoverage();
144
-
145
- await page.goto("/app");
146
-
147
- // Interact to trigger different CSS states
148
- await page.getByRole("button").hover();
149
- await page.getByRole("dialog").waitFor();
150
-
151
- const cssCoverage = await page.coverage.stopCSSCoverage();
152
-
153
- // Find unused CSS
154
- for (const entry of cssCoverage) {
155
- const totalBytes = entry.text?.length || 0;
156
- const usedBytes = entry.ranges.reduce(
157
- (sum, range) => sum + (range.end - range.start),
158
- 0,
159
- );
160
- const unusedPercentage = ((totalBytes - usedBytes) / totalBytes) * 100;
161
-
162
- if (unusedPercentage > 50) {
163
- console.warn(`${entry.url}: ${unusedPercentage.toFixed(1)}% unused CSS`);
164
- }
165
- }
166
- });
167
- ```
168
-
169
- ## Coverage Reports
170
-
171
- ### Converting to Istanbul Format
172
-
173
- ```typescript
174
- // scripts/convert-coverage.ts
175
- import { execSync } from "child_process";
176
- import fs from "fs";
177
- import path from "path";
178
- import v8ToIstanbul from "v8-to-istanbul";
179
-
180
- async function convertCoverage() {
181
- const coverageDir = "./coverage";
182
- const files = fs.readdirSync(coverageDir).filter((f) => f.endsWith(".json"));
183
-
184
- const istanbulCoverage: any = {};
185
-
186
- for (const file of files) {
187
- const coverageData = JSON.parse(
188
- fs.readFileSync(path.join(coverageDir, file), "utf-8"),
189
- );
190
-
191
- for (const entry of coverageData) {
192
- if (!entry.url.startsWith("file://")) continue;
193
-
194
- const filePath = entry.url.replace("file://", "");
195
- const converter = v8ToIstanbul(filePath);
196
-
197
- await converter.load();
198
- converter.applyCoverage(entry.functions || []);
199
-
200
- const istanbul = converter.toIstanbul();
201
- Object.assign(istanbulCoverage, istanbul);
202
- }
203
- }
204
-
205
- fs.writeFileSync(
206
- path.join(coverageDir, "coverage-final.json"),
207
- JSON.stringify(istanbulCoverage),
208
- );
209
- }
210
-
211
- convertCoverage();
212
- ```
213
-
214
- ### Generating HTML Report
215
-
216
- ```bash
217
- # Using nyc to generate report
218
- npx nyc report --reporter=html --reporter=text --temp-dir=./coverage
219
- ```
220
-
221
- ```typescript
222
- // package.json scripts
223
- {
224
- "scripts": {
225
- "test": "playwright test",
226
- "test:coverage": "playwright test && npm run coverage:report",
227
- "coverage:report": "npx nyc report --reporter=html --reporter=lcov --temp-dir=./coverage"
228
- }
229
- }
230
- ```
231
-
232
- ### Custom Coverage Reporter
233
-
234
- ```typescript
235
- // reporters/coverage-reporter.ts
236
- import type { Reporter, FullResult } from "@playwright/test/reporter";
237
- import fs from "fs";
238
- import path from "path";
239
-
240
- class CoverageReporter implements Reporter {
241
- private coverageData: any[] = [];
242
-
243
- onEnd(result: FullResult) {
244
- // Aggregate all coverage files
245
- const coverageDir = "./coverage";
246
- const files = fs
247
- .readdirSync(coverageDir)
248
- .filter((f) => f.endsWith(".json"));
249
-
250
- for (const file of files) {
251
- const data = JSON.parse(
252
- fs.readFileSync(path.join(coverageDir, file), "utf-8"),
253
- );
254
- this.coverageData.push(...data);
255
- }
256
-
257
- // Generate summary
258
- const summary = this.generateSummary();
259
- console.log("\n📊 Coverage Summary:");
260
- console.log(` Files: ${summary.totalFiles}`);
261
- console.log(` Lines: ${summary.lineCoverage.toFixed(1)}%`);
262
- console.log(` Bytes: ${summary.byteCoverage.toFixed(1)}%`);
263
-
264
- if (summary.lineCoverage < 80) {
265
- console.warn("⚠️ Coverage below 80% threshold!");
266
- }
267
- }
268
-
269
- private generateSummary() {
270
- let totalBytes = 0;
271
- let coveredBytes = 0;
272
- const files = new Set<string>();
273
-
274
- for (const entry of this.coverageData) {
275
- if (entry.url.includes("/src/")) {
276
- files.add(entry.url);
277
- totalBytes += entry.text?.length || 0;
278
- coveredBytes += entry.ranges.reduce(
279
- (sum: number, r: any) => sum + (r.end - r.start),
280
- 0,
281
- );
282
- }
283
- }
284
-
285
- return {
286
- totalFiles: files.size,
287
- byteCoverage: (coveredBytes / totalBytes) * 100,
288
- lineCoverage: (coveredBytes / totalBytes) * 100, // Simplified
289
- };
290
- }
291
- }
292
-
293
- export default CoverageReporter;
294
- ```
295
-
296
- ## Coverage Thresholds
297
-
298
- ### Enforcing Minimum Coverage
299
-
300
- ```typescript
301
- // tests/coverage.spec.ts
302
- import { test, expect } from "@playwright/test";
303
- import fs from "fs";
304
- import path from "path";
305
-
306
- test.afterAll(async () => {
307
- const coverageDir = "./coverage";
308
- const files = fs.readdirSync(coverageDir).filter((f) => f.endsWith(".json"));
309
-
310
- let totalBytes = 0;
311
- let coveredBytes = 0;
312
-
313
- for (const file of files) {
314
- const coverage = JSON.parse(
315
- fs.readFileSync(path.join(coverageDir, file), "utf-8"),
316
- );
317
-
318
- for (const entry of coverage) {
319
- if (!entry.url.includes("/src/")) continue;
320
- totalBytes += entry.text?.length || 0;
321
- coveredBytes += entry.ranges.reduce(
322
- (sum: number, r: any) => sum + (r.end - r.start),
323
- 0,
324
- );
325
- }
326
- }
327
-
328
- const coveragePercent = (coveredBytes / totalBytes) * 100;
329
-
330
- // Enforce threshold
331
- expect(coveragePercent).toBeGreaterThan(80);
332
- });
333
- ```
334
-
335
- ### Per-Directory Thresholds
336
-
337
- ```typescript
338
- // coverage-check.ts
339
- interface CoverageThreshold {
340
- pattern: RegExp;
341
- minCoverage: number;
342
- }
343
-
344
- const thresholds: CoverageThreshold[] = [
345
- { pattern: /\/src\/core\//, minCoverage: 90 },
346
- { pattern: /\/src\/utils\//, minCoverage: 85 },
347
- { pattern: /\/src\/components\//, minCoverage: 70 },
348
- { pattern: /\/src\/pages\//, minCoverage: 60 },
349
- ];
350
-
351
- function checkThresholds(coverage: any[]): string[] {
352
- const violations: string[] = [];
353
-
354
- for (const threshold of thresholds) {
355
- const matchingFiles = coverage.filter((c) => threshold.pattern.test(c.url));
356
-
357
- let total = 0;
358
- let covered = 0;
359
-
360
- for (const file of matchingFiles) {
361
- total += file.text?.length || 0;
362
- covered += file.ranges.reduce(
363
- (sum: number, r: any) => sum + (r.end - r.start),
364
- 0,
365
- );
366
- }
367
-
368
- const percent = total > 0 ? (covered / total) * 100 : 0;
369
-
370
- if (percent < threshold.minCoverage) {
371
- violations.push(
372
- `${threshold.pattern}: ${percent.toFixed(1)}% < ${threshold.minCoverage}%`,
373
- );
374
- }
375
- }
376
-
377
- return violations;
378
- }
379
- ```
380
-
381
- ## Advanced Patterns
382
-
383
- ### Merging Coverage Across Shards
384
-
385
- ```typescript
386
- // scripts/merge-coverage.ts
387
- import fs from "fs";
388
- import { glob } from "glob";
389
-
390
- async function mergeCoverage() {
391
- const files = await glob("shard-*/coverage/*.json");
392
- const merged = new Map<string, any>();
393
-
394
- for (const file of files) {
395
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
396
- for (const entry of data) {
397
- if (merged.has(entry.url)) {
398
- const existing = merged.get(entry.url);
399
- existing.ranges.push(...entry.ranges);
400
- } else {
401
- merged.set(entry.url, { ...entry });
402
- }
403
- }
404
- }
405
-
406
- fs.writeFileSync(
407
- "./coverage/merged.json",
408
- JSON.stringify([...merged.values()]),
409
- );
410
- }
411
-
412
- mergeCoverage();
413
- ```
414
-
415
- ### Incremental Coverage
416
-
417
- ```typescript
418
- // Check coverage only for changed files in CI
419
- import { execSync } from "child_process";
420
- import fs from "fs";
421
-
422
- const changedFiles = execSync("git diff --name-only HEAD~1")
423
- .toString()
424
- .split("\n")
425
- .filter((f) => f.endsWith(".ts"));
426
-
427
- const coverage = JSON.parse(fs.readFileSync("./coverage/merged.json", "utf-8"));
428
-
429
- for (const file of changedFiles) {
430
- const entry = coverage.find((c: any) => c.url.includes(file));
431
- if (entry) {
432
- const percent =
433
- (entry.ranges.reduce((s: number, r: any) => s + r.end - r.start, 0) /
434
- (entry.text?.length || 1)) *
435
- 100;
436
- console.log(`${file}: ${percent.toFixed(1)}%`);
437
- }
438
- }
439
- ```
440
-
441
- ## CI Integration
442
-
443
- ### GitHub Actions
444
-
445
- ```yaml
446
- # .github/workflows/test.yml
447
- name: Tests with Coverage
448
-
449
- on: [push, pull_request]
450
-
451
- jobs:
452
- test:
453
- runs-on: ubuntu-latest
454
- steps:
455
- - uses: actions/checkout@v4
456
-
457
- - uses: actions/setup-node@v4
458
- with:
459
- node-version: 20
460
-
461
- - run: npm ci
462
- - run: npx playwright install --with-deps
463
-
464
- - name: Run tests with coverage
465
- run: npm run test:coverage
466
-
467
- - name: Upload coverage to Codecov
468
- uses: codecov/codecov-action@v3
469
- with:
470
- files: ./coverage/lcov.info
471
- fail_ci_if_error: true
472
-
473
- - name: Check coverage threshold
474
- run: |
475
- COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
476
- if (( $(echo "$COVERAGE < 80" | bc -l) )); then
477
- echo "Coverage $COVERAGE% is below 80% threshold"
478
- exit 1
479
- fi
480
- ```
481
-
482
- ## Anti-Patterns to Avoid
483
-
484
- | Anti-Pattern | Problem | Solution |
485
- | ---------------------------- | -------------------------------------- | --------------------------- |
486
- | Coverage for coverage's sake | Gaming metrics | Focus on critical paths |
487
- | 100% coverage target | Diminishing returns, tests for getters | Set realistic thresholds |
488
- | Ignoring coverage drops | Technical debt | Enforce thresholds in CI |
489
- | No source map support | Wrong line numbers | Enable source maps in build |
490
- | Coverage only in CI | Late feedback | Run locally too |
491
-
492
- ## Related References
493
-
494
- - **CI/CD**: See [ci-cd.md](ci-cd.md) for pipeline configuration
495
- - **Performance**: See [performance.md](performance.md) for optimizing coverage collection