Skip to content

Commit

Permalink
Merge pull request #63 from sparksuite/timeout-option
Browse files Browse the repository at this point in the history
Added timeout option
  • Loading branch information
WesCossick authored May 19, 2021
2 parents 76e6da0 + ec58099 commit fd5f6eb
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 17 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "w3c-css-validator",
"version": "1.0.3",
"version": "1.1.0",
"description": "Easily validate CSS using W3C's public CSS validator service",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
30 changes: 28 additions & 2 deletions src/retrieve-validation/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,36 @@
import { W3CCSSValidatorResponse } from '.';

// Utility function for retrieving response from W3C CSS Validator in a browser environment
const retrieveInBrowser = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
const res = await fetch(url);
const retrieveInBrowser = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
// Initialize controller who's signal will abort the fetch
const controller = new AbortController();

// Start timeout
setTimeout(() => {
controller.abort();
}, timeout);

// Attempt to fetch CSS validation, catching the abort error to handle specially
let res: Response | null = null;

try {
res = await fetch(url, { signal: controller.signal });
} catch (err: unknown) {
if (err instanceof Error && err.name === 'AbortError') {
throw new Error(`The request took longer than ${timeout}ms`);
}

throw err;
}

if (!res) {
throw new Error('Response expected');
}

// Parse JSON
const data = (await res.json()) as W3CCSSValidatorResponse;

// Return
return data.cssvalidation;
};

Expand Down
6 changes: 3 additions & 3 deletions src/retrieve-validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export interface W3CCSSValidatorResponse {
}

// Function that detects the appropriate HTTP request client and returns a response accordingly
const retrieveValidation = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
const retrieveValidation = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
if (typeof window !== 'undefined' && typeof window?.fetch === 'function') {
return await retrieveInBrowser(url);
return await retrieveInBrowser(url, timeout);
}

return await retrieveInNode(url);
return await retrieveInNode(url, timeout);
};

export default retrieveValidation;
32 changes: 22 additions & 10 deletions src/retrieve-validation/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ import * as https from 'https';
import { W3CCSSValidatorResponse } from '.';

// Utility function for retrieving response from W3C CSS Validator in a Node.js environment
const retrieveInNode = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
return new Promise((resolve) => {
https.get(url, (res) => {
let data = '';
const retrieveInNode = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
return new Promise((resolve, reject) => {
// Attempt to fetch CSS validation
const req = https.get(
url,
{
timeout,
},
(res) => {
let data = '';

res.on('data', (chunk) => {
data += chunk;
});
res.on('data', (chunk) => {
data += chunk;
});

res.on('end', () => {
resolve((JSON.parse(data) as W3CCSSValidatorResponse).cssvalidation);
});
res.on('end', () => {
resolve((JSON.parse(data) as W3CCSSValidatorResponse).cssvalidation);
});
}
);

// Listen for timeout event and reject in callback
req.on('timeout', () => {
reject(new Error(`The request took longer than ${timeout}ms`));
});
});
};
Expand Down
13 changes: 13 additions & 0 deletions src/validate-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ export default function testValidateText(validateText: ValidateText): void {
);
});

it('Complains about negative timeout', async () => {
await expect(validateText('abc', { timeout: -1 })).rejects.toThrow('The timeout must be a positive integer');
});

it('Complains about non-integer times', async () => {
await expect(validateText('abc', { timeout: Infinity })).rejects.toThrow('The timeout must be an integer');
await expect(validateText('abc', { timeout: 400.1 })).rejects.toThrow('The timeout must be an integer');
});

it('Throws when the timeout is passed', async () => {
await expect(validateText('abc', { timeout: 1 })).rejects.toThrow('The request took longer than 1ms');
});

it('Parses out unwanted characters from error messages', async () => {
const result = await validateText('.foo { foo: bar; }');

Expand Down
13 changes: 12 additions & 1 deletion src/validate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import retrieveValidation from './retrieve-validation';
// Define types
interface ValidateTextOptionsBase {
medium?: 'all' | 'braille' | 'embossed' | 'handheld' | 'print' | 'projection' | 'screen' | 'speech' | 'tty' | 'tv';
timeout?: number;
}

interface ValidateTextOptionsWithoutWarnings extends ValidateTextOptionsBase {
Expand Down Expand Up @@ -58,6 +59,7 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
}

if (options) {
// Validate medium option
const allowedMediums: typeof options['medium'][] = [
'all',
'braille',
Expand All @@ -80,6 +82,15 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
if (options.warningLevel && !allowedWarningLevels.includes(options.warningLevel)) {
throw new Error(`The warning level must be one of the following: ${allowedWarningLevels.join(', ')}`);
}

// Validate timeout option
if (options.timeout !== undefined && !Number.isInteger(options.timeout)) {
throw new Error('The timeout must be an integer');
}

if (options.timeout && options.timeout < 0) {
throw new Error('The timeout must be a positive integer');
}
}

// Build URL for fetching
Expand All @@ -96,7 +107,7 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
.join('&')}`;

// Call W3C CSS Validator API and store response
const cssValidationResponse = await retrieveValidation(url);
const cssValidationResponse = await retrieveValidation(url, options?.timeout ?? 10000);

// Build result
const base: ValidateTextResultBase = {
Expand Down
3 changes: 3 additions & 0 deletions website/docs/functions/validate-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ Option | Default | Possible values
:--- | :--- | :---
`medium` | `all` | `all`, `braille`, `embossed`, `handheld`, `print`, `projection`, `screen`, `speech`, `tty`, `tv`
`warningLevel` | `0` | `0`, `1`, `2`, `3`
`timeout` | `10000` | `integer`

Option | Explanation
:--- | :---
`medium` | The equivalent of the `@media` rule, applied to all of the CSS
`warningLevel` | `0` means don’t return any warnings; `1`, `2`, `3` will return warnings (if any), with higher numbers corresponding to more warnings
`timeout` | The time in milliseconds after which the request to the W3C API will be terminated and an error will be thrown

```ts
const result = await cssValidator.validateText(css, {
medium: 'print',
warningLevel: 3,
timeout: 3000,
});
```

Expand Down

0 comments on commit fd5f6eb

Please sign in to comment.