@realfavicongenerator/check-favicon 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/fixtures/180x180.png +0 -0
- package/fixtures/192x192.png +0 -0
- package/fixtures/512x512.png +0 -0
- package/fixtures/happy-face.svg +1 -0
- package/fixtures/logo-transparent.png +0 -0
- package/fixtures/non-square.png +0 -0
- package/jest.config.js +5 -0
- package/package.json +38 -0
- package/src/desktop/desktop.test.ts +101 -0
- package/src/desktop/desktop.ts +207 -0
- package/src/desktop/ico.ts +110 -0
- package/src/helper.test.ts +128 -0
- package/src/helper.ts +170 -0
- package/src/index.ts +161 -0
- package/src/test-helper.ts +12 -0
- package/src/touch-icon.test.ts +219 -0
- package/src/touch-icon.ts +205 -0
- package/src/web-manifest.test.ts +196 -0
- package/src/web-manifest.ts +261 -0
- package/tsconfig.json +109 -0
package/src/helper.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Fetcher, pathToMimeType } from '.';
|
|
4
|
+
import sharp, { FormatEnum } from 'sharp';
|
|
5
|
+
|
|
6
|
+
export const filePathToReadableStream = async (path: string): Promise<ReadableStream> => {
|
|
7
|
+
const file = await fs.open(path, 'r');
|
|
8
|
+
const stream = file.createReadStream();
|
|
9
|
+
|
|
10
|
+
return new ReadableStream({
|
|
11
|
+
start(controller) {
|
|
12
|
+
stream.on('data', (chunk) => {
|
|
13
|
+
controller.enqueue(chunk);
|
|
14
|
+
});
|
|
15
|
+
stream.on('close', () => {
|
|
16
|
+
controller.close();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const filePathToString = async (path: string): Promise<string> => (
|
|
23
|
+
fs.readFile(path, 'utf-8')
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export const stringToReadableStream = (str: string): ReadableStream => {
|
|
27
|
+
return new ReadableStream({
|
|
28
|
+
start(controller) {
|
|
29
|
+
controller.enqueue(str);
|
|
30
|
+
controller.close();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const readableStreamToString = async (readableStream: ReadableStream): Promise<string> => {
|
|
36
|
+
const reader = readableStream.getReader();
|
|
37
|
+
const chunks: Uint8Array[] = [];
|
|
38
|
+
let done = false;
|
|
39
|
+
while (!done) {
|
|
40
|
+
const { value, done: doneValue } = await reader.read();
|
|
41
|
+
done = doneValue;
|
|
42
|
+
if (value) {
|
|
43
|
+
chunks.push(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const concatenatedChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
concatenatedChunks.set(chunk, offset);
|
|
50
|
+
offset += chunk.length;
|
|
51
|
+
}
|
|
52
|
+
return new TextDecoder("utf-8").decode(concatenatedChunks);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const readableStreamToBuffer = async (readableStream: ReadableStream): Promise<Buffer> => {
|
|
56
|
+
const reader = readableStream.getReader();
|
|
57
|
+
const chunks: Uint8Array[] = [];
|
|
58
|
+
let done = false;
|
|
59
|
+
while (!done) {
|
|
60
|
+
const { value, done: doneValue } = await reader.read();
|
|
61
|
+
done = doneValue;
|
|
62
|
+
if (value) {
|
|
63
|
+
chunks.push(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const concatenatedChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
|
|
67
|
+
let offset = 0;
|
|
68
|
+
for (const chunk of chunks) {
|
|
69
|
+
concatenatedChunks.set(chunk, offset);
|
|
70
|
+
offset += chunk.length;
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(concatenatedChunks);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type CheckIconProcessor = {
|
|
76
|
+
noHref: () => void,
|
|
77
|
+
icon404: () => void,
|
|
78
|
+
cannotGet: (httpStatusCode: number) => void,
|
|
79
|
+
downloadable: () => void,
|
|
80
|
+
square: (widthHeight: number) => void,
|
|
81
|
+
notSquare: (width: number, Height: number) => void,
|
|
82
|
+
rightSize: (widthHeight: number) => void,
|
|
83
|
+
wrongSize: (widthHeight: number) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const checkIcon = async (
|
|
87
|
+
iconUrl: string | undefined,
|
|
88
|
+
processor: CheckIconProcessor,
|
|
89
|
+
fetcher: Fetcher,
|
|
90
|
+
mimeType: string | undefined,
|
|
91
|
+
expectedWidthHeight?: number
|
|
92
|
+
): Promise<string | null> => {
|
|
93
|
+
if (!iconUrl) {
|
|
94
|
+
processor.noHref();
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const res = await fetcher(iconUrl, mimeType);
|
|
99
|
+
if (res.status === 404) {
|
|
100
|
+
processor.icon404();
|
|
101
|
+
} else if (res.status >= 300) {
|
|
102
|
+
processor.cannotGet(res.status);
|
|
103
|
+
} else if (res.readableStream) {
|
|
104
|
+
processor.downloadable();
|
|
105
|
+
|
|
106
|
+
const content = await readableStreamToBuffer(res.readableStream);
|
|
107
|
+
const meta = await sharp(content).metadata();
|
|
108
|
+
|
|
109
|
+
const contentType = res.contentType || pathToMimeType(iconUrl);
|
|
110
|
+
|
|
111
|
+
if (meta.width && meta.height) {
|
|
112
|
+
if (meta.width !== meta.height) {
|
|
113
|
+
processor.notSquare(meta.width, meta.height);
|
|
114
|
+
return null;
|
|
115
|
+
} else {
|
|
116
|
+
processor.square(meta.width);
|
|
117
|
+
|
|
118
|
+
if (expectedWidthHeight) {
|
|
119
|
+
if (meta.width === expectedWidthHeight) {
|
|
120
|
+
processor.rightSize(meta.width);
|
|
121
|
+
} else {
|
|
122
|
+
processor.wrongSize(meta.width);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return bufferToDataUrl(content, contentType);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const mergeUrlAndPath = (baseUrl: string, absoluteOrRelativePath: string): string => {
|
|
135
|
+
// If the path is a full URL, return it as is
|
|
136
|
+
if (absoluteOrRelativePath.startsWith('http://') || absoluteOrRelativePath.startsWith('https://')) {
|
|
137
|
+
return absoluteOrRelativePath;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const url = new URL(baseUrl);
|
|
141
|
+
|
|
142
|
+
// If the path starts with a slash, replace the pathname
|
|
143
|
+
if (absoluteOrRelativePath.startsWith('/')) {
|
|
144
|
+
return `${url.origin}${absoluteOrRelativePath}`;
|
|
145
|
+
} else {
|
|
146
|
+
// Otherwise, append the path to the existing pathname
|
|
147
|
+
return `${url.href}${url.href.endsWith('/') ? '' : '/'}${absoluteOrRelativePath}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const parseSizesAttribute = (sizes: string | undefined | null): number | null => {
|
|
152
|
+
if (!sizes) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const match = sizes.match(/(\d+)x(\d+)/);
|
|
157
|
+
if (match) {
|
|
158
|
+
if (match[1] !== match[2]) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return parseInt(match[1]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const bufferToDataUrl = (buffer: Buffer, mimeType: string): string => {
|
|
169
|
+
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
170
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
|
|
2
|
+
export enum CheckerStatus {
|
|
3
|
+
Ok = 'Ok',
|
|
4
|
+
Error = 'Error',
|
|
5
|
+
Warning = 'Warning'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum MessageId {
|
|
9
|
+
noHead,
|
|
10
|
+
|
|
11
|
+
svgFaviconDeclared,
|
|
12
|
+
noSvgFavicon,
|
|
13
|
+
multipleSvgFavicons,
|
|
14
|
+
noSvgFaviconHref,
|
|
15
|
+
svgFavicon404,
|
|
16
|
+
svgFaviconCannotGet,
|
|
17
|
+
svgFaviconDownloadable,
|
|
18
|
+
svgFaviconSquare,
|
|
19
|
+
svgFaviconNotSquare,
|
|
20
|
+
|
|
21
|
+
noIcoFavicon,
|
|
22
|
+
multipleIcoFavicons,
|
|
23
|
+
icoFaviconDeclared,
|
|
24
|
+
noIcoFaviconHref,
|
|
25
|
+
icoFavicon404,
|
|
26
|
+
icoFaviconCannotGet,
|
|
27
|
+
icoFaviconDownloadable,
|
|
28
|
+
icoFaviconExtraSizes,
|
|
29
|
+
icoFaviconMissingSizes,
|
|
30
|
+
icoFaviconExpectedSizes,
|
|
31
|
+
|
|
32
|
+
noDesktopPngFavicon,
|
|
33
|
+
no48x48DesktopPngFavicon,
|
|
34
|
+
desktopPngFaviconDeclared,
|
|
35
|
+
noDesktopPngFaviconHref,
|
|
36
|
+
desktopPngFaviconCannotGet,
|
|
37
|
+
desktopPngFaviconDownloadable,
|
|
38
|
+
desktopPngFavicon404,
|
|
39
|
+
desktopPngFaviconWrongSize,
|
|
40
|
+
desktopPngFaviconRightSize,
|
|
41
|
+
|
|
42
|
+
noTouchWebAppTitle,
|
|
43
|
+
multipleTouchWebAppTitles,
|
|
44
|
+
emptyTouchWebAppTitle,
|
|
45
|
+
touchWebAppTitleDeclared,
|
|
46
|
+
noTouchIcon,
|
|
47
|
+
duplicatedTouchIconSizes,
|
|
48
|
+
touchIconWithSize,
|
|
49
|
+
touchIconDeclared,
|
|
50
|
+
noTouchIconHref,
|
|
51
|
+
touchIcon404,
|
|
52
|
+
touchIconCannotGet,
|
|
53
|
+
touchIconDownloadable,
|
|
54
|
+
touchIconSquare,
|
|
55
|
+
touchIcon180x180,
|
|
56
|
+
touchIconNotSquare,
|
|
57
|
+
touchIconWrongSize,
|
|
58
|
+
|
|
59
|
+
noManifest,
|
|
60
|
+
noManifestHref,
|
|
61
|
+
manifest404,
|
|
62
|
+
manifestCannotGet,
|
|
63
|
+
manifestInvalidJson,
|
|
64
|
+
manifestName,
|
|
65
|
+
noManifestName,
|
|
66
|
+
manifestShortName,
|
|
67
|
+
noManifestShortName,
|
|
68
|
+
manifestBackgroundColor,
|
|
69
|
+
noManifestBackgroundColor,
|
|
70
|
+
manifestThemeColor,
|
|
71
|
+
noManifestThemeColor,
|
|
72
|
+
noManifestIcons,
|
|
73
|
+
noManifestIcon,
|
|
74
|
+
manifestIconDeclared,
|
|
75
|
+
manifestIconCannotGet,
|
|
76
|
+
manifestIconDownloadable,
|
|
77
|
+
manifestIcon404,
|
|
78
|
+
manifestIconNoHref,
|
|
79
|
+
manifestIconNotSquare,
|
|
80
|
+
manifestIconRightSize,
|
|
81
|
+
manifestIconSquare,
|
|
82
|
+
manifestIconWrongSize
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type CheckerMessage = {
|
|
86
|
+
status: CheckerStatus,
|
|
87
|
+
id: MessageId,
|
|
88
|
+
text: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type FetchResponse = {
|
|
92
|
+
status: number,
|
|
93
|
+
contentType: string | null,
|
|
94
|
+
readableStream?: ReadableStream | null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type Fetcher = (url: string, contentType?: string) => Promise<FetchResponse>;
|
|
98
|
+
|
|
99
|
+
export const fetchFetcher: Fetcher = async (url, contentType) => {
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': contentType || pathToMimeType(url),
|
|
103
|
+
'user-agent': 'RealFaviconGenerator Favicon Checker'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status: res.status,
|
|
109
|
+
contentType: res.headers.get('Content-Type') || null,
|
|
110
|
+
readableStream: res.body
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type DesktopFaviconReport = {
|
|
115
|
+
messages: CheckerMessage[],
|
|
116
|
+
icon: string | null,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type TouchIconTitleReport = {
|
|
120
|
+
messages: CheckerMessage[],
|
|
121
|
+
appTitle?: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type TouchIconIconReport = {
|
|
125
|
+
messages: CheckerMessage[],
|
|
126
|
+
touchIcon: string | null,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type WebManifestReport = {
|
|
130
|
+
messages: CheckerMessage[],
|
|
131
|
+
name?: string,
|
|
132
|
+
shortName?: string,
|
|
133
|
+
backgroundColor?: string,
|
|
134
|
+
themeColor?: string,
|
|
135
|
+
icon: string | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type FaviconReport = {
|
|
139
|
+
desktop: DesktopFaviconReport,
|
|
140
|
+
touchIcon: TouchIconReport,
|
|
141
|
+
webAppManifest: WebManifestReport
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type TouchIconReport = TouchIconIconReport & TouchIconTitleReport;
|
|
145
|
+
|
|
146
|
+
export const pathToMimeType = (path: string): string => {
|
|
147
|
+
const ext = path.split('.').pop();
|
|
148
|
+
switch (ext) {
|
|
149
|
+
case 'png':
|
|
150
|
+
return 'image/png';
|
|
151
|
+
case 'svg':
|
|
152
|
+
return 'image/svg+xml';
|
|
153
|
+
case 'ico':
|
|
154
|
+
return 'image/x-icon';
|
|
155
|
+
case 'jpg':
|
|
156
|
+
case 'jpeg':
|
|
157
|
+
return 'image/jpeg';
|
|
158
|
+
default:
|
|
159
|
+
return 'application/octet-stream';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CheckerMessage, FetchResponse, Fetcher } from ".";
|
|
2
|
+
import { parse } from 'node-html-parser'
|
|
3
|
+
|
|
4
|
+
export const testFetcher = (database: { [url: string]: FetchResponse }): Fetcher => {
|
|
5
|
+
return async (url, contentType) => {
|
|
6
|
+
const res = database[url];
|
|
7
|
+
if (!res) {
|
|
8
|
+
return { status: 404, contentType: contentType || 'application/octet-stream' };
|
|
9
|
+
}
|
|
10
|
+
return res;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { parse } from 'node-html-parser'
|
|
2
|
+
import { CheckerMessage, CheckerStatus, FetchResponse, MessageId } from ".";
|
|
3
|
+
import { checkTouchIcon, checkTouchIconIcon, checkTouchIconTitle, getDuplicatedSizes } from "./touch-icon";
|
|
4
|
+
import { testFetcher } from './test-helper';
|
|
5
|
+
import { bufferToDataUrl, filePathToReadableStream, readableStreamToBuffer } from './helper';
|
|
6
|
+
|
|
7
|
+
type TestOutput = {
|
|
8
|
+
messages: Pick<CheckerMessage, 'id' | 'status'>[],
|
|
9
|
+
appTitle?: string,
|
|
10
|
+
touchIcon?: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const runCheckTouchIconTitleTest = async (
|
|
14
|
+
headFragment: string | null,
|
|
15
|
+
output: TestOutput,
|
|
16
|
+
fetchDatabase: { [url: string]: FetchResponse } = {}
|
|
17
|
+
) => {
|
|
18
|
+
const root = headFragment ? parse(headFragment) : null;
|
|
19
|
+
const result = await checkTouchIconTitle('https://example.com/', root, testFetcher(fetchDatabase));
|
|
20
|
+
const filteredMessages = result.messages.map(m => ({ status: m.status, id: m.id }));
|
|
21
|
+
expect({
|
|
22
|
+
messages: filteredMessages,
|
|
23
|
+
appTitle: result.appTitle
|
|
24
|
+
}).toEqual(output);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('checkTouchIconTitle - noHead', async () => {
|
|
28
|
+
await runCheckTouchIconTitleTest(null, { messages: [{
|
|
29
|
+
status: CheckerStatus.Error,
|
|
30
|
+
id: MessageId.noHead,
|
|
31
|
+
}]});
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('checkTouchIconTitle - noTouchWebAppTitle', async () => {
|
|
35
|
+
await runCheckTouchIconTitleTest('<title>Some text</title>', { messages: [{
|
|
36
|
+
status: CheckerStatus.Warning,
|
|
37
|
+
id: MessageId.noTouchWebAppTitle,
|
|
38
|
+
}]});
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('checkTouchIconTitle - multipleTouchWebAppTitles', async () => {
|
|
42
|
+
await runCheckTouchIconTitleTest(`
|
|
43
|
+
<meta name="apple-mobile-web-app-title" content="First title">
|
|
44
|
+
<meta name="apple-mobile-web-app-title" content="Second title">
|
|
45
|
+
`, { messages: [{
|
|
46
|
+
status: CheckerStatus.Error,
|
|
47
|
+
id: MessageId.multipleTouchWebAppTitles,
|
|
48
|
+
}]});
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('checkTouchIconTitle - touchWebAppTitleDeclared', async () => {
|
|
52
|
+
await runCheckTouchIconTitleTest(`
|
|
53
|
+
<meta name="apple-mobile-web-app-title" content="The App Name">
|
|
54
|
+
`, { messages: [{
|
|
55
|
+
status: CheckerStatus.Ok,
|
|
56
|
+
id: MessageId.touchWebAppTitleDeclared,
|
|
57
|
+
}], appTitle: 'The App Name' });
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const runCheckTouchIconTest = async (
|
|
61
|
+
headFragment: string | null,
|
|
62
|
+
output: TestOutput,
|
|
63
|
+
fetchDatabase: { [url: string]: FetchResponse } = {}
|
|
64
|
+
) => {
|
|
65
|
+
const root = headFragment ? parse(headFragment) : null;
|
|
66
|
+
const result = await checkTouchIconIcon('https://example.com/', root, testFetcher(fetchDatabase));
|
|
67
|
+
const filteredMessages = result.messages.map(m => ({ status: m.status, id: m.id }));
|
|
68
|
+
expect({
|
|
69
|
+
messages: filteredMessages,
|
|
70
|
+
touchIcon: result.touchIcon
|
|
71
|
+
}).toEqual({
|
|
72
|
+
...output,
|
|
73
|
+
touchIcon: output.touchIcon || null
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('checkTouchIcon - noHead', async () => {
|
|
78
|
+
await runCheckTouchIconTest(null, { messages: [{
|
|
79
|
+
status: CheckerStatus.Error,
|
|
80
|
+
id: MessageId.noHead,
|
|
81
|
+
}]});
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('checkTouchIcon - noTouchIcon', async () => {
|
|
85
|
+
await runCheckTouchIconTest('<title>Some text</title>', { messages: [{
|
|
86
|
+
status: CheckerStatus.Error,
|
|
87
|
+
id: MessageId.noTouchIcon,
|
|
88
|
+
}]});
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('checkTouchIcon - touchIconWithSize', async () => {
|
|
92
|
+
await runCheckTouchIconTest(`
|
|
93
|
+
<link rel="apple-touch-icon" sizes="152x152" href="some-other-icon.png">
|
|
94
|
+
`, { messages: [{
|
|
95
|
+
status: CheckerStatus.Ok,
|
|
96
|
+
id: MessageId.touchIconDeclared,
|
|
97
|
+
}, {
|
|
98
|
+
status: CheckerStatus.Warning,
|
|
99
|
+
id: MessageId.touchIconWithSize,
|
|
100
|
+
}]}, {
|
|
101
|
+
'https://example.com/some-other-icon.png': {
|
|
102
|
+
status: 200,
|
|
103
|
+
contentType: 'image/png',
|
|
104
|
+
readableStream: null
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('checkTouchIcon - multipleTouchIcon - no size', async () => {
|
|
110
|
+
await runCheckTouchIconTest(`
|
|
111
|
+
<link rel="apple-touch-icon" href="some-icon.png">
|
|
112
|
+
<link rel="apple-touch-icon" href="some-other-icon.png">
|
|
113
|
+
`, { messages: [{
|
|
114
|
+
status: CheckerStatus.Ok,
|
|
115
|
+
id: MessageId.touchIconDeclared,
|
|
116
|
+
}, {
|
|
117
|
+
status: CheckerStatus.Error,
|
|
118
|
+
id: MessageId.duplicatedTouchIconSizes,
|
|
119
|
+
}]}, {
|
|
120
|
+
'https://example.com/some-icon.png': {
|
|
121
|
+
status: 200,
|
|
122
|
+
contentType: 'image/png',
|
|
123
|
+
readableStream: null
|
|
124
|
+
},
|
|
125
|
+
'https://example.com/some-other-icon.png': {
|
|
126
|
+
status: 200,
|
|
127
|
+
contentType: 'image/png',
|
|
128
|
+
readableStream: null
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('checkTouchIcon - multipleTouchIcon - specific size', async () => {
|
|
134
|
+
await runCheckTouchIconTest(`
|
|
135
|
+
<link rel="apple-touch-icon" sizes="180x180" href="some-icon.png">
|
|
136
|
+
<link rel="apple-touch-icon" sizes="180x180" href="some-other-icon.png">
|
|
137
|
+
`, { messages: [{
|
|
138
|
+
status: CheckerStatus.Ok,
|
|
139
|
+
id: MessageId.touchIconDeclared,
|
|
140
|
+
}, {
|
|
141
|
+
status: CheckerStatus.Warning,
|
|
142
|
+
id: MessageId.touchIconWithSize,
|
|
143
|
+
}, {
|
|
144
|
+
status: CheckerStatus.Error,
|
|
145
|
+
id: MessageId.duplicatedTouchIconSizes,
|
|
146
|
+
}]}, {
|
|
147
|
+
'https://example.com/some-icon.png': {
|
|
148
|
+
status: 200,
|
|
149
|
+
contentType: 'image/png',
|
|
150
|
+
readableStream: null
|
|
151
|
+
},
|
|
152
|
+
'https://example.com/some-other-icon.png': {
|
|
153
|
+
status: 200,
|
|
154
|
+
contentType: 'image/png',
|
|
155
|
+
readableStream: null
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('checkTouchIcon - touchIconWithSize', async () => {
|
|
161
|
+
await runCheckTouchIconTest(`
|
|
162
|
+
<link rel="apple-touch-icon" sizes="180x180" href="some-other-icon.png">
|
|
163
|
+
`, { messages: [{
|
|
164
|
+
status: CheckerStatus.Ok,
|
|
165
|
+
id: MessageId.touchIconDeclared,
|
|
166
|
+
}, {
|
|
167
|
+
status: CheckerStatus.Warning,
|
|
168
|
+
id: MessageId.touchIconWithSize,
|
|
169
|
+
}]}, {
|
|
170
|
+
'https://example.com/some-other-icon.png': {
|
|
171
|
+
status: 200,
|
|
172
|
+
contentType: 'image/png',
|
|
173
|
+
readableStream: null
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const testIcon = './fixtures/180x180.png';
|
|
179
|
+
|
|
180
|
+
test('checkTouchIcon - Regular case', async () => {
|
|
181
|
+
await runCheckTouchIconTest(`
|
|
182
|
+
<link rel="apple-touch-icon" href="some-other-icon.png">
|
|
183
|
+
`, { messages: [{
|
|
184
|
+
status: CheckerStatus.Ok,
|
|
185
|
+
id: MessageId.touchIconDeclared,
|
|
186
|
+
}, {
|
|
187
|
+
status: CheckerStatus.Ok,
|
|
188
|
+
id: MessageId.touchIconDownloadable,
|
|
189
|
+
},{
|
|
190
|
+
status: CheckerStatus.Ok,
|
|
191
|
+
id: MessageId.touchIconSquare
|
|
192
|
+
}], touchIcon: bufferToDataUrl(await readableStreamToBuffer(await filePathToReadableStream(testIcon)), 'image/png')
|
|
193
|
+
}, {
|
|
194
|
+
'https://example.com/some-other-icon.png': {
|
|
195
|
+
status: 200,
|
|
196
|
+
contentType: 'image/png',
|
|
197
|
+
readableStream: await filePathToReadableStream(testIcon)
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
test('getDuplicatedSizes', () => {
|
|
205
|
+
// No duplicates
|
|
206
|
+
expect(getDuplicatedSizes([])).toEqual([]);
|
|
207
|
+
expect(getDuplicatedSizes([ undefined ])).toEqual([]);
|
|
208
|
+
expect(getDuplicatedSizes([ '180x180' ])).toEqual([]);
|
|
209
|
+
expect(getDuplicatedSizes([ undefined, '180x180' ])).toEqual([]);
|
|
210
|
+
|
|
211
|
+
// Duplicates
|
|
212
|
+
expect(getDuplicatedSizes([ '152x152', '180x180', '180x180' ])).toEqual([ '180x180' ]);
|
|
213
|
+
expect(getDuplicatedSizes([ undefined, '180x180', undefined, undefined ])).toEqual([ undefined ]);
|
|
214
|
+
expect(getDuplicatedSizes([
|
|
215
|
+
'152x152', '180x180', '152x152', undefined, '152x152', undefined
|
|
216
|
+
])).toEqual([
|
|
217
|
+
'152x152', undefined
|
|
218
|
+
]);
|
|
219
|
+
})
|