Skip to content

Commit 70e67c1

Browse files
committed
chore: use zod to validate annotations
1 parent b57d673 commit 70e67c1

File tree

8 files changed

+122
-20
lines changed

8 files changed

+122
-20
lines changed

packages/playwright-core/ThirdPartyNotices.txt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ This project incorporates components from the projects listed below. The origina
4848
- [email protected] (https://github.com/eemeli/yaml)
4949
- [email protected] (https://github.com/thejoshwolfe/yauzl)
5050
- [email protected] (https://github.com/thejoshwolfe/yazl)
51+
- [email protected] (https://github.com/colinhacks/zod)
5152

5253
%% [email protected] NOTICES AND INFORMATION BEGIN HERE
5354
=========================================
@@ -1127,8 +1128,34 @@ SOFTWARE.
11271128
=========================================
11281129
END OF [email protected] AND INFORMATION
11291130

1131+
%% [email protected] NOTICES AND INFORMATION BEGIN HERE
1132+
=========================================
1133+
MIT License
1134+
1135+
Copyright (c) 2025 Colin McDonnell
1136+
1137+
Permission is hereby granted, free of charge, to any person obtaining a copy
1138+
of this software and associated documentation files (the "Software"), to deal
1139+
in the Software without restriction, including without limitation the rights
1140+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1141+
copies of the Software, and to permit persons to whom the Software is
1142+
furnished to do so, subject to the following conditions:
1143+
1144+
The above copyright notice and this permission notice shall be included in all
1145+
copies or substantial portions of the Software.
1146+
1147+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1148+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1149+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1150+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1151+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1152+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1153+
SOFTWARE.
1154+
=========================================
1155+
END OF [email protected] AND INFORMATION
1156+
11301157
SUMMARY BEGIN HERE
11311158
=========================================
1132-
Total Packages: 44
1159+
Total Packages: 45
11331160
=========================================
11341161
END OF SUMMARY

packages/playwright-core/bundles/utils/package-lock.json

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/playwright-core/bundles/utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"signal-exit": "3.0.7",
2222
"socks-proxy-agent": "8.0.5",
2323
"ws": "8.17.1",
24-
"yaml": "^2.6.0"
24+
"yaml": "^2.6.0",
25+
"zod": "^3.25.76"
2526
},
2627
"devDependencies": {
2728
"@types/debug": "^4.1.7",

packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,15 @@ export const progress = progressLibrary;
5757

5858
export { SocksProxyAgent } from 'socks-proxy-agent';
5959

60-
import yamlLibrary from 'yaml';
61-
export const yaml = yamlLibrary;
62-
6360
// @ts-ignore
6461
import wsLibrary, { WebSocketServer, Receiver, Sender } from 'ws';
6562
export const ws = wsLibrary;
6663
export const wsServer = WebSocketServer;
6764
export const wsReceiver = Receiver;
6865
export const wsSender = Sender;
66+
67+
import yamlLibrary from 'yaml';
68+
export const yaml = yamlLibrary;
69+
70+
import zodLibrary from 'zod';
71+
export const zod = zodLibrary;

packages/playwright-core/src/utilsBundle.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ export const program: typeof import('../bundles/utils/node_modules/commander').p
3030
export const ProgramOption: typeof import('../bundles/utils/node_modules/commander').Option = require('./utilsBundleImpl').ProgramOption;
3131
export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress;
3232
export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent;
33-
export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml;
34-
export type { Range as YAMLRange, Scalar as YAMLScalar, YAMLError, YAMLMap, YAMLSeq } from '../bundles/utils/node_modules/yaml';
3533
export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws;
3634
export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer;
3735
export const wsReceiver = require('./utilsBundleImpl').wsReceiver;
3836
export const wsSender = require('./utilsBundleImpl').wsSender;
37+
export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml;
38+
export type { Range as YAMLRange, Scalar as YAMLScalar, YAMLError, YAMLMap, YAMLSeq } from '../bundles/utils/node_modules/yaml';
39+
export const zod: typeof import('../bundles/utils/node_modules/zod') = require('./utilsBundleImpl').zod;
3940
export type { Command } from '../bundles/utils/node_modules/commander';
4041
export type { EventEmitter as WebSocketEventEmitter, RawData as WebSocketRawData, WebSocket, WebSocketServer } from '../bundles/utils/node_modules/@types/ws';
4142

packages/playwright/src/common/testType.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuit
2121
import { Suite, TestCase } from './test';
2222
import { expect } from '../matchers/expect';
2323
import { wrapFunctionWithLocation } from '../transform/transform';
24+
import { validateTestDetails } from './validators';
2425

2526
import type { FixturesWithLocation } from './config';
2627
import type { Fixtures, TestDetails, TestStepInfo, TestType } from '../../types/test';
@@ -309,17 +310,6 @@ function throwIfRunningInsideJest() {
309310
}
310311
}
311312

312-
function validateTestDetails(details: TestDetails, location: Location) {
313-
const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
314-
const annotations = originalAnnotations.map(annotation => ({ ...annotation, location }));
315-
const tags = Array.isArray(details.tag) ? details.tag : (details.tag ? [details.tag] : []);
316-
for (const tag of tags) {
317-
if (tag[0] !== '@')
318-
throw new Error(`Tag must start with "@" symbol, got "${tag}" instead.`);
319-
}
320-
return { annotations, tags };
321-
}
322-
323313
export const rootTestType = new TestTypeImpl([]);
324314

325315
export function mergeTests(...tests: TestType<any, any>[]) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { zod } from 'playwright-core/lib/utilsBundle';
18+
19+
import type { TestAnnotation, TestDetailsAnnotation } from 'packages/playwright/types/test';
20+
import type { Location } from '../../types/testReporter';
21+
import type { ZodError } from 'zod';
22+
23+
const testAnnotationSchema = zod.object({
24+
type: zod.string(),
25+
description: zod.string().optional(),
26+
});
27+
28+
const testDetailsSchema = zod.object({
29+
tag: zod.union([
30+
zod.string().optional(),
31+
zod.array(zod.string())
32+
]).transform(val => Array.isArray(val) ? val : val !== undefined ? [val] : []).refine(val => val.every(v => v.startsWith('@')), {
33+
message: "Tag must start with '@'"
34+
}),
35+
annotation: zod.union([
36+
testAnnotationSchema,
37+
zod.array(testAnnotationSchema).optional()
38+
]).transform(val => Array.isArray(val) ? val : val !== undefined ? [val] : []),
39+
});
40+
41+
export function validateTestAnnotation(annotation: unknown): TestAnnotation {
42+
try {
43+
return testAnnotationSchema.parse(annotation);
44+
} catch (error) {
45+
throwZodError(error);
46+
}
47+
}
48+
49+
type ValidTestDetails = {
50+
tags: string[];
51+
annotations: (TestDetailsAnnotation & { location: Location })[];
52+
location: Location;
53+
};
54+
55+
export function validateTestDetails(details: unknown, location: Location): ValidTestDetails {
56+
try {
57+
const parsedDetails = testDetailsSchema.parse(details);
58+
return {
59+
annotations: parsedDetails.annotation.map(a => ({ ...a, location })),
60+
tags: parsedDetails.tag,
61+
location,
62+
};
63+
} catch (error) {
64+
throwZodError(error);
65+
}
66+
}
67+
68+
function throwZodError(error: any): never {
69+
throw new Error((error as ZodError).issues.map(i => i.message).join('\n'));
70+
}

tests/playwright-test/test-tag.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ test('should enforce @ symbol', async ({ runInlineTest }) => {
148148
`
149149
});
150150
expect(result.exitCode).toBe(1);
151-
expect(result.output).toContain(`Error: Tag must start with "@" symbol, got "foo" instead.`);
151+
expect(result.output).toContain(`Error: Tag must start with '@'`);
152152
});
153153

154154
test('should be included in testInfo', async ({ runInlineTest }, testInfo) => {

0 commit comments

Comments
 (0)