Skip to content

Commit c440592

Browse files
authored
Merge pull request #14 from sparksuite/initial-implementation
Initial implementation of #validateText
2 parents 58e9641 + 60f7046 commit c440592

File tree

11 files changed

+372
-12
lines changed

11 files changed

+372
-12
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test": "yarn --frozen-lockfile --check-files && yarn compile && yarn link && cd test/puppeteer && yarn --frozen-lockfile --check-files && yarn link \"w3c-css-validator\" && npx webpack && cd ../../ && jest",
1212
"lint": "eslint --ext .js,.ts . && prettier --check '**/*.{ts,js,json,yml}'",
1313
"format": "eslint --fix --ext .js,.ts . && prettier --write '**/*.{ts,js,json,yml}'",
14-
"clean": "rm -rf node_modules/ coverage/ dist/",
14+
"clean": "git clean -X -d --force && find . -type d -empty -delete",
1515
"compile": "rm -rf dist/ && tsc"
1616
},
1717
"repository": {

src/index.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1-
export default function helloWorld(): string {
2-
return 'Hello world!';
3-
}
1+
// Imports
2+
import validateText from './validate-text';
3+
4+
// Validates CSS using W3C's public CSS validator service
5+
const cssValidator = {
6+
validateText,
7+
};
8+
9+
export default cssValidator;

src/retrieve-validation/browser.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Imports
2+
import { W3CCSSValidatorResponse } from '.';
3+
4+
// Utility function for retrieving response from W3C CSS Validator in a browser environment
5+
const retrieveInBrowser = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
6+
const res = await fetch(url);
7+
const data = (await res.json()) as W3CCSSValidatorResponse;
8+
9+
return data.cssvalidation;
10+
};
11+
12+
export default retrieveInBrowser;

src/retrieve-validation/index.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Imports
2+
import retrieveInBrowser from './browser';
3+
import retrieveInNode from './node';
4+
5+
// Define types
6+
export interface W3CCSSValidatorResponse {
7+
cssvalidation: {
8+
validity: boolean;
9+
errors?: {
10+
line: number;
11+
message: string;
12+
}[];
13+
warnings?: {
14+
line: number;
15+
level: 0 | 1 | 2;
16+
message: string;
17+
}[];
18+
};
19+
}
20+
21+
// Function that detects the appropriate HTTP request client and returns a response accordingly
22+
const retrieveValidation = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
23+
if (typeof window?.fetch === 'function') {
24+
return await retrieveInBrowser(url);
25+
}
26+
27+
return await retrieveInNode(url);
28+
};
29+
30+
export default retrieveValidation;

src/retrieve-validation/node.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Imports
2+
import * as https from 'https';
3+
import { W3CCSSValidatorResponse } from '.';
4+
5+
// Utility function for retrieving response from W3C CSS Validator in a Node.js environment
6+
const retrieveInNode = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
7+
return new Promise((resolve) => {
8+
https.get(url, (res) => {
9+
let data = '';
10+
11+
res.on('data', (chunk) => {
12+
data += chunk;
13+
});
14+
15+
res.on('end', () => {
16+
resolve((JSON.parse(data) as W3CCSSValidatorResponse).cssvalidation);
17+
});
18+
});
19+
});
20+
};
21+
22+
export default retrieveInNode;

src/validate-text.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Imports
2+
import retrieveValidation from './retrieve-validation';
3+
4+
// Define types
5+
interface ValidateTextOptionsBase {
6+
medium?: 'all' | 'braille' | 'embossed' | 'handheld' | 'print' | 'projection' | 'screen' | 'speech' | 'tty' | 'tv';
7+
}
8+
9+
interface ValidateTextOptionsWithoutWarnings extends ValidateTextOptionsBase {
10+
warningLevel?: 0;
11+
}
12+
13+
interface ValidateTextOptionsWithWarnings extends ValidateTextOptionsBase {
14+
warningLevel: 1 | 2 | 3;
15+
}
16+
17+
type ValidateTextOptions = ValidateTextOptionsWithWarnings | ValidateTextOptionsWithoutWarnings;
18+
19+
interface ValidateTextResultBase {
20+
valid: boolean;
21+
errors: {
22+
line: number;
23+
message: string;
24+
}[];
25+
}
26+
27+
interface ValidateTextResultWithWarnings extends ValidateTextResultBase {
28+
warnings: {
29+
line: number;
30+
level: 1 | 2 | 3;
31+
message: string;
32+
}[];
33+
}
34+
35+
interface ValidateTextResultWithoutWarnings extends ValidateTextResultBase {
36+
warnings?: never;
37+
}
38+
39+
type ValidateTextResult = ValidateTextResultWithWarnings | ValidateTextResultWithoutWarnings;
40+
41+
// Validates a string of CSS
42+
async function validateText(
43+
textToValidate: string,
44+
options?: ValidateTextOptionsWithoutWarnings
45+
): Promise<ValidateTextResultWithoutWarnings>;
46+
async function validateText(
47+
textToValidate: string,
48+
options: ValidateTextOptionsWithWarnings
49+
): Promise<ValidateTextResultWithWarnings>;
50+
async function validateText(textToBeValidated: string, options?: ValidateTextOptions): Promise<ValidateTextResult> {
51+
// Build URL for fetching
52+
const params = {
53+
text: encodeURIComponent(textToBeValidated),
54+
usermedium: options?.medium ?? 'all',
55+
warning: options?.warningLevel ? options.warningLevel - 1 : 'no',
56+
output: 'application/json',
57+
};
58+
59+
const url = `https://jigsaw.w3.org/css-validator/validator?${Object.entries(params)
60+
.map(([key, val]) => `${key}=${val}`)
61+
.join('&')}`;
62+
63+
// Call W3C CSS Validator API and store response
64+
const cssValidationResponse = await retrieveValidation(url);
65+
66+
// Build result
67+
const base: ValidateTextResultBase = {
68+
valid: false,
69+
errors: [],
70+
};
71+
const result: ValidateTextResultWithWarnings | ValidateTextResultBase = options?.warningLevel
72+
? {
73+
...base,
74+
warnings: [],
75+
}
76+
: base;
77+
78+
result.valid = cssValidationResponse.validity;
79+
80+
cssValidationResponse.errors?.forEach((error) => {
81+
result.errors.push({
82+
line: error.line,
83+
message: error.message,
84+
});
85+
});
86+
87+
if ('warnings' in result) {
88+
cssValidationResponse.warnings?.forEach((warning) => {
89+
result.warnings.push({
90+
line: warning.line,
91+
message: warning.message,
92+
level: (warning.level + 1) as 1 | 2 | 3,
93+
});
94+
});
95+
}
96+
97+
// Return
98+
return result;
99+
}
100+
101+
export default validateText;

test/index.test.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,52 @@
1-
import helloWorld from '../src/index';
1+
// Imports
2+
import cssValidator from '../src';
23

3-
describe('#helloWorld()', () => {
4-
it('Works', () => {
5-
expect(helloWorld()).toEqual('Hello world!');
4+
// Tests
5+
describe('#validateText()', () => {
6+
afterEach(
7+
() => new Promise<void>((resolve) => setTimeout(resolve, 1000))
8+
);
9+
10+
it('Returns the validity and errors when no options are provided', async () => {
11+
expect(await cssValidator.validateText('.foo { text-align: center; }')).toStrictEqual({
12+
valid: true,
13+
errors: [],
14+
});
15+
});
16+
17+
it('Returns the validity, errors, and warnings when a warning level option is provided', async () => {
18+
expect(await cssValidator.validateText('.foo { text-align: center; }', { warningLevel: 1 })).toStrictEqual({
19+
valid: true,
20+
errors: [],
21+
warnings: [],
22+
});
23+
});
24+
25+
it('Includes errors present in the response on the result', async () => {
26+
expect(await cssValidator.validateText('.foo { text-align: center; ')).toStrictEqual({
27+
valid: false,
28+
errors: [
29+
{
30+
line: 1,
31+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32+
message: expect.any(String),
33+
},
34+
],
35+
});
36+
});
37+
38+
it('Includes warnings present in the response on the result when options specify a warning level', async () => {
39+
expect(await cssValidator.validateText('.foo { font-family: Georgia; }', { warningLevel: 3 })).toStrictEqual({
40+
valid: true,
41+
errors: [],
42+
warnings: [
43+
{
44+
level: 3,
45+
line: 1,
46+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
47+
message: expect.any(String),
48+
},
49+
],
50+
});
651
});
752
});

test/puppeteer/index.html

+14-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@
77
</head>
88

99
<body>
10-
<h1 id='is-valid'></h1>
10+
<div id='input'>
11+
<textarea id='custom-css'></textarea>
12+
<select id='warning-level'>
13+
<option>0</option>
14+
<option>1</option>
15+
<option>2</option>
16+
<option>3</option>
17+
</select>
18+
</div>
19+
<div id='output'>
20+
<h1 id='is-valid'></h1>
21+
<ul id='errors'></ul>
22+
<ul id='warnings'></ul>
23+
</div>
1124
<button id='make-call'></button>
1225
</body>
1326
</html>

test/puppeteer/index.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
// Helper functions used in multiple tests
2+
const waitForResponse = async (options?: { expectErrors?: true; expectWarnings?: true }): Promise<void> => {
3+
await page.waitForFunction(
4+
"document.querySelector('#is-valid').innerText === 'true' || document.querySelector('#is-valid').innerText === 'false'"
5+
);
6+
7+
if (options?.expectErrors) {
8+
await page.waitForFunction("document.querySelector('#errors').childElementCount > 0");
9+
}
10+
11+
if (options?.expectWarnings) {
12+
await page.waitForFunction("document.querySelector('#warnings').childElementCount > 0");
13+
}
14+
};
15+
116
// Setup work before each test
217
beforeEach(async () => {
318
await jestPuppeteer.resetPage();
@@ -7,7 +22,43 @@ beforeEach(async () => {
722
});
823
});
924

25+
// Wait after each test, see "Note" section under https://jigsaw.w3.org/css-validator/manual.html#expert
26+
afterEach(
27+
() => new Promise<void>((resolve) => setTimeout(resolve, 1000))
28+
);
29+
1030
// Tests
1131
it('Loads', async () => {
1232
await expect(page.title()).resolves.toMatch('Hello world!');
1333
});
34+
35+
it('Returns the validity', async () => {
36+
await page.type('#custom-css', '.foo { text-align: center; }');
37+
await page.click('#make-call');
38+
39+
await waitForResponse();
40+
expect(await page.evaluate(() => document.querySelector<HTMLHeadingElement>('#is-valid').innerText)).toBe('true');
41+
});
42+
43+
it('Includes errors present in the response on the result', async () => {
44+
await page.type('#custom-css', '.foo { text-align: center; ');
45+
await page.click('#make-call');
46+
47+
await waitForResponse({ expectErrors: true });
48+
expect(await page.evaluate(() => document.querySelector<HTMLHeadingElement>('#is-valid').innerText)).toBe('false');
49+
expect(
50+
await page.evaluate(() => document.querySelector<HTMLUListElement>('#errors').childElementCount)
51+
).toBeGreaterThan(0);
52+
});
53+
54+
it('Includes warnings present in the response on the result when options specify a warning level', async () => {
55+
await page.type('#custom-css', '.foo { font-family: Georgia; }');
56+
await page.select('#warning-level', '3');
57+
await page.click('#make-call');
58+
59+
await waitForResponse({ expectWarnings: true });
60+
expect(await page.evaluate(() => document.querySelector<HTMLHeadingElement>('#is-valid').innerText)).toBe('true');
61+
expect(
62+
await page.evaluate(() => document.querySelector<HTMLUListElement>('#warnings').childElementCount)
63+
).toBeGreaterThan(0);
64+
});

0 commit comments

Comments
 (0)