Skip to content

Commit ef5a5be

Browse files
committed
feat(models): add URL source type for issues
1 parent ebd8f37 commit ef5a5be

File tree

6 files changed

+243
-7
lines changed

6 files changed

+243
-7
lines changed

packages/models/docs/models-reference.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,20 @@ _Object containing the following properties:_
291291

292292
_(\*) Required._
293293

294+
## FileIssue
295+
296+
Issue with a file source location
297+
298+
_Object containing the following properties:_
299+
300+
| Property | Description | Type |
301+
| :------------------ | :------------------------ | :---------------------------------------- |
302+
| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) |
303+
| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) |
304+
| **`source`** (\*) | Source file location | [SourceFileLocation](#sourcefilelocation) |
305+
306+
_(\*) Required._
307+
294308
## FileName
295309

296310
_String which matches the regular expression `/^(?!.*[ \\/:*?"<>|]).+$/` and has a minimum length of 1._
@@ -374,11 +388,11 @@ Issue information
374388

375389
_Object containing the following properties:_
376390

377-
| Property | Description | Type |
378-
| :------------------ | :------------------------ | :---------------------------------------- |
379-
| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) |
380-
| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) |
381-
| `source` | Source file location | [SourceFileLocation](#sourcefilelocation) |
391+
| Property | Description | Type |
392+
| :------------------ | :--------------------------------------------- | :------------------------------ |
393+
| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) |
394+
| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) |
395+
| `source` | Source location of an issue (file path or URL) | [IssueSource](#issuesource) |
382396

383397
_(\*) Required._
384398

@@ -392,6 +406,15 @@ _Enum, one of the following possible values:_
392406
- `'warning'`
393407
- `'error'`
394408

409+
## IssueSource
410+
411+
Source location of an issue (file path or URL)
412+
413+
_Union of the following possible types:_
414+
415+
- [SourceFileLocation](#sourcefilelocation)
416+
- [SourceUrlLocation](#sourceurllocation)
417+
395418
## MaterialIcon
396419

397420
Icon from VSCode Material Icons extension
@@ -1500,6 +1523,20 @@ _Object containing the following properties:_
15001523

15011524
_(\*) Required._
15021525

1526+
## SourceUrlLocation
1527+
1528+
Location of a DOM element in a web page
1529+
1530+
_Object containing the following properties:_
1531+
1532+
| Property | Description | Type |
1533+
| :------------- | :-------------------------------------------- | :--------------- |
1534+
| **`url`** (\*) | URL of the web page where the issue was found | `string` (_url_) |
1535+
| `snippet` | HTML snippet of the element | `string` |
1536+
| `selector` | CSS selector to locate the element | `string` |
1537+
1538+
_(\*) Required._
1539+
15031540
## TableAlignment
15041541

15051542
Cell alignment
@@ -1579,6 +1616,20 @@ _Object containing the following properties:_
15791616

15801617
_(\*) Required._
15811618

1619+
## UrlIssue
1620+
1621+
Issue with a URL source location
1622+
1623+
_Object containing the following properties:_
1624+
1625+
| Property | Description | Type |
1626+
| :------------------ | :-------------------------------------- | :-------------------------------------- |
1627+
| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) |
1628+
| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) |
1629+
| **`source`** (\*) | Location of a DOM element in a web page | [SourceUrlLocation](#sourceurllocation) |
1630+
1631+
_(\*) Required._
1632+
15821633
## Weight
15831634

15841635
Coefficient for the given score (use weight 0 if only for display)

packages/models/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ export {
44
} from './lib/implementation/schemas.js';
55
export {
66
sourceFileLocationSchema,
7+
sourceUrlLocationSchema,
8+
issueSourceSchema,
9+
type IssueSource,
710
type SourceFileLocation,
11+
type SourceUrlLocation,
812
} from './lib/source.js';
913

1014
export {
@@ -80,10 +84,14 @@ export {
8084
validateAsync,
8185
} from './lib/implementation/validate.js';
8286
export {
87+
fileIssueSchema,
8388
issueSchema,
8489
issueSeveritySchema,
90+
urlIssueSchema,
91+
type FileIssue,
8592
type Issue,
8693
type IssueSeverity,
94+
type UrlIssue,
8795
} from './lib/issue.js';
8896
export {
8997
formatSchema,

packages/models/src/lib/issue.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { z } from 'zod';
22
import { MAX_ISSUE_MESSAGE_LENGTH } from './implementation/limits.js';
3-
import { sourceFileLocationSchema } from './source.js';
3+
import {
4+
issueSourceSchema,
5+
sourceFileLocationSchema,
6+
sourceUrlLocationSchema,
7+
} from './source.js';
48

59
export const issueSeveritySchema = z.enum(['info', 'warning', 'error']).meta({
610
title: 'IssueSeverity',
@@ -15,10 +19,26 @@ export const issueSchema = z
1519
.max(MAX_ISSUE_MESSAGE_LENGTH)
1620
.meta({ description: 'Descriptive error message' }),
1721
severity: issueSeveritySchema,
18-
source: sourceFileLocationSchema.optional(),
22+
source: issueSourceSchema.optional(),
1923
})
2024
.meta({
2125
title: 'Issue',
2226
description: 'Issue information',
2327
});
2428
export type Issue = z.infer<typeof issueSchema>;
29+
30+
export const fileIssueSchema = issueSchema
31+
.extend({ source: sourceFileLocationSchema })
32+
.meta({
33+
title: 'FileIssue',
34+
description: 'Issue with a file source location',
35+
});
36+
export type FileIssue = z.infer<typeof fileIssueSchema>;
37+
38+
export const urlIssueSchema = issueSchema
39+
.extend({ source: sourceUrlLocationSchema })
40+
.meta({
41+
title: 'UrlIssue',
42+
description: 'Issue with a URL source location',
43+
});
44+
export type UrlIssue = z.infer<typeof urlIssueSchema>;

packages/models/src/lib/issue.unit.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ describe('issueSchema', () => {
2323
).not.toThrow();
2424
});
2525

26+
it('should accept a valid issue with source URL information', () => {
27+
expect(() =>
28+
issueSchema.parse({
29+
message: 'Image is missing alt attribute',
30+
severity: 'error',
31+
source: {
32+
url: 'https://example.com/page',
33+
snippet: '<img src="logo.png">',
34+
selector: 'img.logo',
35+
},
36+
}),
37+
).not.toThrow();
38+
});
39+
40+
it('should accept issue with URL source without optional fields', () => {
41+
expect(() =>
42+
issueSchema.parse({
43+
message: 'Accessibility issue found',
44+
severity: 'warning',
45+
source: {
46+
url: 'https://example.com',
47+
},
48+
}),
49+
).not.toThrow();
50+
});
51+
2652
it('should throw for a missing message', () => {
2753
expect(() =>
2854
issueSchema.parse({

packages/models/src/lib/source.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from 'zod';
22
import {
33
filePathSchema,
44
filePositionSchema,
5+
urlSchema,
56
} from './implementation/schemas.js';
67

78
export const sourceFileLocationSchema = z
@@ -17,3 +18,31 @@ export const sourceFileLocationSchema = z
1718
});
1819

1920
export type SourceFileLocation = z.infer<typeof sourceFileLocationSchema>;
21+
22+
export const sourceUrlLocationSchema = z
23+
.object({
24+
url: urlSchema.meta({
25+
description: 'URL of the web page where the issue was found',
26+
}),
27+
snippet: z.string().optional().meta({
28+
description: 'HTML snippet of the element',
29+
}),
30+
selector: z.string().optional().meta({
31+
description: 'CSS selector to locate the element',
32+
}),
33+
})
34+
.meta({
35+
title: 'SourceUrlLocation',
36+
description: 'Location of a DOM element in a web page',
37+
});
38+
39+
export type SourceUrlLocation = z.infer<typeof sourceUrlLocationSchema>;
40+
41+
export const issueSourceSchema = z
42+
.union([sourceFileLocationSchema, sourceUrlLocationSchema])
43+
.meta({
44+
title: 'IssueSource',
45+
description: 'Source location of an issue (file path or URL)',
46+
});
47+
48+
export type IssueSource = z.infer<typeof issueSourceSchema>;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
issueSourceSchema,
3+
sourceFileLocationSchema,
4+
sourceUrlLocationSchema,
5+
} from './source.js';
6+
7+
describe('sourceFileLocationSchema', () => {
8+
it('should accept valid file location with position', () => {
9+
expect(() =>
10+
sourceFileLocationSchema.parse({
11+
file: 'src/index.ts',
12+
position: { startLine: 10, endLine: 15 },
13+
}),
14+
).not.toThrow();
15+
});
16+
17+
it('should accept file location without position', () => {
18+
expect(() =>
19+
sourceFileLocationSchema.parse({ file: 'src/utils.ts' }),
20+
).not.toThrow();
21+
});
22+
23+
it('should reject empty file path', () => {
24+
expect(() => sourceFileLocationSchema.parse({ file: '' })).toThrow(
25+
'Too small',
26+
);
27+
});
28+
});
29+
30+
describe('sourceUrlLocationSchema', () => {
31+
it('should accept valid URL location with all fields', () => {
32+
expect(() =>
33+
sourceUrlLocationSchema.parse({
34+
url: 'https://example.com/page',
35+
snippet: '<img src="logo.png">',
36+
selector: 'img.logo',
37+
}),
38+
).not.toThrow();
39+
});
40+
41+
it('should accept URL location with only required url field', () => {
42+
expect(() =>
43+
sourceUrlLocationSchema.parse({ url: 'https://example.com' }),
44+
).not.toThrow();
45+
});
46+
47+
it('should accept URL location with snippet only', () => {
48+
expect(() =>
49+
sourceUrlLocationSchema.parse({
50+
url: 'https://example.com/dashboard',
51+
snippet: '<button disabled>Submit</button>',
52+
}),
53+
).not.toThrow();
54+
});
55+
56+
it('should accept URL location with selector only', () => {
57+
expect(() =>
58+
sourceUrlLocationSchema.parse({
59+
url: 'https://example.com/form',
60+
selector: '#submit-btn',
61+
}),
62+
).not.toThrow();
63+
});
64+
65+
it('should reject invalid URL', () => {
66+
expect(() =>
67+
sourceUrlLocationSchema.parse({ url: 'not-a-valid-url' }),
68+
).toThrow('Invalid URL');
69+
});
70+
71+
it('should reject missing URL', () => {
72+
expect(() =>
73+
sourceUrlLocationSchema.parse({ snippet: '<div>No URL provided</div>' }),
74+
).toThrow('Invalid input');
75+
});
76+
});
77+
78+
describe('issueSourceSchema', () => {
79+
it('should accept file-based source', () => {
80+
expect(() =>
81+
issueSourceSchema.parse({
82+
file: 'src/app.ts',
83+
position: { startLine: 1 },
84+
}),
85+
).not.toThrow();
86+
});
87+
88+
it('should accept URL-based source', () => {
89+
expect(() =>
90+
issueSourceSchema.parse({
91+
url: 'https://example.com',
92+
selector: '#main',
93+
}),
94+
).not.toThrow();
95+
});
96+
97+
it('should reject source with neither file nor url', () => {
98+
expect(() =>
99+
issueSourceSchema.parse({ position: { startLine: 1 } }),
100+
).toThrow('Invalid input');
101+
});
102+
});

0 commit comments

Comments
 (0)