Skip to content

Commit 22380ff

Browse files
committed
Replace test fixture export with plain save_css_coverage() function
Avoids importing @playwright/test entirely, removing the implicit dependency that would require it as a peerDependency or dependency in the published package. Users wire save_css_coverage() into their own Playwright fixture, keeping full control over the fixture lifecycle. Tests no longer need a browser — mock coverage data is passed directly to save_css_coverage(), making them faster and simpler.
1 parent e394783 commit 22380ff

4 files changed

Lines changed: 93 additions & 130 deletions

File tree

‎README.md‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,28 @@ let report = calculcate_coverage(coverage)
5858

5959
### Playwright fixture
6060

61-
Use the built-in Playwright fixture to collect CSS coverage per test and write results to disk.
61+
Use `save_css_coverage` to collect and save CSS coverage from within a Playwright fixture.
6262

63-
**1. Extend your fixtures file** (e.g. `tests/fixtures.ts`):
63+
**1. Add a fixture** (e.g. `tests/fixtures.ts`):
6464

6565
```ts
66-
import { mergeTests, test as base, expect } from '@playwright/test'
67-
import { test as withCssCoverage } from '@projectwallace/css-code-coverage/playwright'
68-
69-
export const test = mergeTests(base, withCssCoverage)
66+
import { test as base, expect } from '@playwright/test'
67+
import { save_css_coverage } from '@projectwallace/css-code-coverage/playwright'
68+
69+
export const test = base.extend({
70+
cssCoverage: [
71+
async ({ page }, use, testInfo) => {
72+
await page.coverage.startCSSCoverage()
73+
await use()
74+
let coverage = await page.coverage.stopCSSCoverage()
75+
await save_css_coverage(coverage, {
76+
title_path: testInfo.titlePath,
77+
attach: testInfo.attach.bind(testInfo),
78+
})
79+
},
80+
{},
81+
],
82+
})
7083
export { expect }
7184
```
7285

@@ -94,18 +107,17 @@ test('first test', async ({ page }) => { ... })
94107
test('second test', async ({ page }) => { ... })
95108
```
96109

97-
**3. Configure the output directory** (optional) in `playwright.config.ts`:
110+
**Configure the output directory** (optional) via the `dir` option:
98111

99112
```ts
100-
import { defineConfig } from '@playwright/test'
101-
102-
export default defineConfig({
103-
use: {
104-
cssCoverageDir: 'css-coverage', // default
105-
},
113+
await save_css_coverage(coverage, {
114+
dir: 'css-coverage', // default
115+
title_path: testInfo.titlePath,
106116
})
107117
```
108118

119+
Or set it globally in `playwright.config.ts` by passing the configured path to `save_css_coverage` from a shared fixture.
120+
109121
Pass the directory to the `css-coverage` CLI to analyze results:
110122

111123
```sh

‎src/playwright.test.ts‎

Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,74 @@
1-
import { mergeTests, test as base, expect } from '@playwright/test'
2-
import { test as withCssCoverage, slugify } from './playwright/index.js'
1+
import { test, expect } from '@playwright/test'
2+
import { save_css_coverage, slugify } from './playwright/index.js'
3+
import type { Coverage } from './lib/parse-coverage.js'
34
import * as fs from 'node:fs/promises'
45
import * as os from 'node:os'
56
import * as path from 'node:path'
67

7-
const test = mergeTests(base, withCssCoverage)
8+
const MOCK_COVERAGE: Coverage[] = [
9+
{
10+
url: 'http://example.com/style.css',
11+
text: 'body { color: red }',
12+
ranges: [{ start: 0, end: 19 }],
13+
},
14+
]
815

9-
const TEST_CONTENT = '<style>body { margin: 0 }</style><body></body>'
10-
11-
function expected_filename(title_path: string[]): string {
12-
return title_path.map(slugify).join('-') + '.json'
13-
}
14-
15-
test.describe('cssCoverageDir option', () => {
16-
let custom_dir = path.join(os.tmpdir(), 'css-code-coverage-fixture-dir-test')
17-
let filename = ''
18-
19-
test.use({ cssCoverageDir: custom_dir })
16+
test.describe('save_css_coverage', () => {
17+
let dir = path.join(os.tmpdir(), 'css-code-coverage-save-test')
2018

2119
test.beforeAll(async () => {
22-
await fs.rm(custom_dir, { recursive: true, force: true })
23-
})
24-
25-
test('writes coverage to the configured directory', async ({ page, cssCoverage }, testInfo) => {
26-
await page.setContent(TEST_CONTENT)
27-
filename = expected_filename(testInfo.titlePath)
20+
await fs.rm(dir, { recursive: true, force: true })
2821
})
2922

3023
test.afterAll(async () => {
31-
await expect(fs.access(path.join(custom_dir, filename))).resolves.toBeUndefined()
32-
await fs.rm(custom_dir, { recursive: true, force: true })
24+
await fs.rm(dir, { recursive: true, force: true })
3325
})
34-
})
35-
36-
test.describe('default cssCoverageDir', () => {
37-
let default_dir = path.join(process.cwd(), 'css-coverage')
38-
let filename = ''
3926

40-
test('defaults to css-coverage relative to cwd', async ({ page, cssCoverage }, testInfo) => {
41-
await page.setContent(TEST_CONTENT)
42-
filename = expected_filename(testInfo.titlePath)
27+
test('writes file to the configured directory', async () => {
28+
let title_path = ['writes file to the configured directory']
29+
await save_css_coverage(MOCK_COVERAGE, { dir, title_path })
30+
await expect(
31+
fs.access(path.join(dir, title_path.map(slugify).join('-') + '.json')),
32+
).resolves.toBeUndefined()
4333
})
4434

45-
test.afterAll(async () => {
46-
await expect(fs.access(path.join(default_dir, filename))).resolves.toBeUndefined()
35+
test('defaults dir to css-coverage relative to cwd', async () => {
36+
let default_dir = path.join(process.cwd(), 'css-coverage')
37+
let title_path = ['defaults dir to css-coverage relative to cwd']
38+
await save_css_coverage(MOCK_COVERAGE, { title_path })
39+
await expect(
40+
fs.access(path.join(default_dir, title_path.map(slugify).join('-') + '.json')),
41+
).resolves.toBeUndefined()
4742
await fs.rm(default_dir, { recursive: true, force: true })
4843
})
49-
})
50-
51-
test.describe('file naming', () => {
52-
let dir = path.join(os.tmpdir(), 'css-code-coverage-fixture-naming-test')
53-
let filename = ''
54-
55-
test.use({ cssCoverageDir: dir })
56-
57-
test.beforeAll(async () => {
58-
await fs.rm(dir, { recursive: true, force: true })
59-
})
6044

61-
test('derives filename from the test title path', async ({ page, cssCoverage }, testInfo) => {
62-
await page.setContent(TEST_CONTENT)
63-
filename = expected_filename(testInfo.titlePath)
64-
})
65-
66-
test.afterAll(async () => {
45+
test('derives filename from title_path', async () => {
46+
let title_path = ['My Suite', 'my test/name.ts']
47+
await save_css_coverage(MOCK_COVERAGE, { dir, title_path })
6748
let files = await fs.readdir(dir)
68-
expect(files).toContain(filename)
69-
await fs.rm(dir, { recursive: true, force: true })
70-
})
71-
})
72-
73-
test.describe('file content', () => {
74-
let dir = path.join(os.tmpdir(), 'css-code-coverage-fixture-content-test')
75-
76-
test.use({ cssCoverageDir: dir })
77-
78-
test.beforeAll(async () => {
79-
await fs.rm(dir, { recursive: true, force: true })
80-
})
81-
82-
test('writes valid JSON', async ({ page, cssCoverage }) => {
83-
await page.setContent(TEST_CONTENT)
49+
expect(files).toContain('my-suite-my-test-name-ts.json')
8450
})
8551

86-
test('content is an array of coverage entries', async ({ page, cssCoverage }) => {
87-
await page.setContent('<style>h1 { color: red }</style><body><h1>hi</h1></body>')
52+
test('calls attach with correct arguments', async () => {
53+
let attached: { name: string; path: string; contentType: string } | undefined
54+
let title_path = ['calls attach with correct arguments']
55+
await save_css_coverage(MOCK_COVERAGE, {
56+
dir,
57+
title_path,
58+
attach: async (name, opts) => {
59+
attached = { name, ...opts }
60+
},
61+
})
62+
expect(attached?.name).toBe('css-coverage')
63+
expect(attached?.contentType).toBe('application/json')
64+
expect(attached?.path).toContain('.json')
8865
})
8966

90-
test.afterAll(async () => {
91-
let files = await fs.readdir(dir)
92-
for (let file of files) {
93-
let content = await fs.readFile(path.join(dir, file), 'utf-8')
94-
let parsed = JSON.parse(content)
95-
expect(Array.isArray(parsed)).toBe(true)
96-
for (let entry of parsed) {
97-
expect(entry).toMatchObject({
98-
url: expect.any(String),
99-
text: expect.any(String),
100-
ranges: expect.any(Array),
101-
})
102-
}
103-
}
104-
await fs.rm(dir, { recursive: true, force: true })
67+
test('file content matches the coverage input', async () => {
68+
let title_path = ['file content matches the coverage input']
69+
await save_css_coverage(MOCK_COVERAGE, { dir, title_path })
70+
let file_path = path.join(dir, title_path.map(slugify).join('-') + '.json')
71+
let content = JSON.parse(await fs.readFile(file_path, 'utf-8'))
72+
expect(content).toEqual(MOCK_COVERAGE)
10573
})
10674
})

‎src/playwright/index.ts‎

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,32 @@
1-
import { test as base_test } from '@playwright/test'
21
import * as fs from 'node:fs/promises'
32
import * as path from 'node:path'
4-
5-
type CssCoverageFixtures = {
6-
cssCoverage: void
7-
}
8-
9-
type CssCoverageOptions = {
10-
cssCoverageDir: string
11-
}
3+
import type { Coverage } from '../lib/parse-coverage.js'
124

135
// Needed because titlePath entries can contain '/' (creates subdirs), spaces,
146
// dots, and other chars that are invalid or problematic in file names.
157
export function slugify(s: string): string {
168
return s
179
.replaceAll(/\s+|\/|\./g, '-')
18-
.replaceAll(/[^a-zA-Z0-9-_]/g, '')
10+
.replaceAll(/[^a-z0-9-_]/gi, '')
1911
.toLowerCase()
2012
}
2113

22-
export const test = base_test.extend<CssCoverageFixtures, CssCoverageOptions>({
23-
cssCoverageDir: ['css-coverage', { option: true }],
14+
export async function save_css_coverage(
15+
coverage: Coverage[],
16+
options: {
17+
dir?: string
18+
title_path: string[]
19+
attach?: (name: string, options: { path: string; contentType: string }) => Promise<void>
20+
},
21+
): Promise<void> {
22+
let { dir = 'css-coverage', title_path, attach } = options
2423

25-
cssCoverage: [
26-
async ({ page, cssCoverageDir }, use, testInfo) => {
27-
await page.coverage.startCSSCoverage()
28-
await use()
29-
let coverage = await page.coverage.stopCSSCoverage()
24+
let file_name = title_path.map(slugify).join('-') + '.json'
25+
let resolved_dir = path.resolve(process.cwd(), dir)
26+
await fs.mkdir(resolved_dir, { recursive: true })
27+
let file_path = path.join(resolved_dir, file_name)
3028

31-
let file_name = testInfo.titlePath.map(slugify).join('-') + '.json'
29+
await fs.writeFile(file_path, JSON.stringify(coverage))
3230

33-
let dir = path.resolve(process.cwd(), cssCoverageDir)
34-
await fs.mkdir(dir, { recursive: true })
35-
let file_path = path.join(dir, file_name)
36-
37-
await fs.writeFile(file_path, JSON.stringify(coverage))
38-
39-
await testInfo.attach('css-coverage', {
40-
path: file_path,
41-
contentType: 'application/json',
42-
})
43-
},
44-
{},
45-
],
46-
})
31+
await attach?.('css-coverage', { path: file_path, contentType: 'application/json' })
32+
}

‎tsdown.config.ts‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ export default defineConfig([
2222
format: 'esm',
2323
outDir: 'dist/playwright',
2424
publint: true,
25-
deps: {
26-
neverBundle: ['@playwright/test'],
27-
},
2825
plugins: [
2926
codecovVitePlugin({
3027
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,

0 commit comments

Comments
 (0)