Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0bde3b3e6 | ||
|
|
73c5b3383e | ||
|
|
5f09fb3bde | ||
|
|
0610fd6fbb | ||
|
|
85c685a5ca | ||
|
|
2977506cb8 | ||
|
|
538d734a5b | ||
|
|
e680f429eb | ||
|
|
c308850eb3 | ||
|
|
8e941233a0 | ||
|
|
22a8a206ab | ||
|
|
773249ca4f | ||
|
|
599d02ea0e | ||
|
|
675316c9b7 | ||
|
|
c22fd8b132 | ||
|
|
c4b008f910 | ||
|
|
0cde5a3607 | ||
|
|
6180b9f86c | ||
|
|
daae7d8523 | ||
|
|
540e5b5ac0 | ||
|
|
4b671001e0 | ||
|
|
1ae4bed339 | ||
|
|
04e734bd5a | ||
|
|
a0e71ef444 | ||
|
|
05d8da36d8 | ||
|
|
5cae61b896 | ||
|
|
a319e84831 | ||
|
|
d38781400e | ||
|
|
cbcc6647f2 | ||
|
|
d2a217319d | ||
|
|
280b564ab3 | ||
|
|
0e14b5bd04 | ||
|
|
90ded3e1cb | ||
|
|
e795b3369d | ||
|
|
2f26c7db96 | ||
|
|
ed588ba43c | ||
|
|
c9f63138a0 | ||
|
|
97f779dc83 | ||
|
|
2753446a57 | ||
|
|
163eef58c2 | ||
|
|
201bf75bf7 | ||
|
|
dd4d0d4607 | ||
|
|
88bbaea4c9 | ||
|
|
ac37ebb4e2 | ||
|
|
16c3abf3f2 | ||
|
|
d1681c3d88 | ||
|
|
97099e9f65 | ||
|
|
4e8cb19f55 | ||
|
|
f7280c1a90 | ||
|
|
bfd6c5d2bd | ||
|
|
47eab96a67 | ||
|
|
ec7cba8f49 | ||
|
|
e0d497796a | ||
|
|
c5bc02d45d | ||
|
|
e2b713282c | ||
|
|
05c883fa3e | ||
|
|
e5d9680b23 | ||
|
|
4a40483348 | ||
|
|
5f12d1ce0e | ||
|
|
050b2303f0 | ||
|
|
a579f5736b | ||
|
|
82294983b0 | ||
|
|
8558ea29df | ||
|
|
9211074b32 | ||
|
|
2da4371847 | ||
|
|
791bf9fc8d | ||
|
|
e4b324d209 | ||
|
|
3b0bd172ba | ||
|
|
91e1724540 | ||
|
|
fcdf3c9240 | ||
|
|
92d92b8021 | ||
|
|
dc5bd2e71e | ||
|
|
36fc0f0e10 | ||
|
|
52a781dee8 | ||
|
|
15d60a3a7b | ||
|
|
0a1b9ce27a | ||
|
|
bb9429e907 | ||
|
|
1213854644 | ||
|
|
e3d4100055 | ||
|
|
418426419d | ||
|
|
707048a1df | ||
|
|
e9ab1fd16e | ||
|
|
cc74359e83 | ||
|
|
9be862e37b | ||
|
|
2133c1e16a | ||
|
|
e46d4cff27 | ||
|
|
38b362e5fe | ||
|
|
6d01461e53 | ||
|
|
942dd89ea3 | ||
|
|
d40d4ec7fb | ||
|
|
96f720c195 | ||
|
|
3f5c54d5de | ||
|
|
c62805ae57 | ||
|
|
acb1b72036 | ||
|
|
443f452689 | ||
|
|
72a347aa9f | ||
|
|
ab7ffa0d94 | ||
|
|
1259439603 | ||
|
|
208ed63c96 | ||
|
|
dd439d5ec3 | ||
|
|
44a14f326b | ||
|
|
4d4a9523f8 | ||
|
|
b24a676f22 | ||
|
|
4e692d608c | ||
|
|
b54eb0cca9 | ||
|
|
24ad2f45cc | ||
|
|
27b5ce8d60 | ||
|
|
1ce350b12e | ||
|
|
405d53220b | ||
|
|
3123e90d5a | ||
|
|
ea7a9811e4 | ||
|
|
74b04ff922 | ||
|
|
4b24cd68ce | ||
|
|
c94089842a | ||
|
|
9c0743d668 | ||
|
|
d023af20a0 | ||
|
|
b81ddd8dd9 | ||
|
|
c1e7f58567 | ||
|
|
8c055d0b72 | ||
|
|
44a30f7a0d | ||
|
|
f837ad2270 | ||
|
|
6b909bd0e3 | ||
|
|
23d7dea0bb | ||
|
|
fc13053545 | ||
|
|
e874ccb5b1 | ||
|
|
fd11e8c1fe | ||
|
|
294c5ab06d | ||
|
|
576dac3aff | ||
|
|
4b80310c0d | ||
|
|
bd9cb2e257 | ||
|
|
64733d55f8 | ||
|
|
88483af607 | ||
|
|
1726e023ea | ||
|
|
7e9b12f818 | ||
|
|
834a9c9ac5 | ||
|
|
3f098cd33a | ||
|
|
4193d61e0b | ||
|
|
e8d464a244 | ||
|
|
4c1c375eef | ||
|
|
65a2bbae0a | ||
|
|
57616aba62 | ||
|
|
4396bd4625 | ||
|
|
df4f5685be | ||
|
|
1e5f797195 | ||
|
|
8f06ceaedd | ||
|
|
6b58b72028 | ||
|
|
51dc1b1618 | ||
|
|
be5549bf81 | ||
|
|
c4b7bd4d71 | ||
|
|
f25eb2901e | ||
|
|
65025b57c5 | ||
|
|
8e3b212df0 | ||
|
|
d93af3d6e4 | ||
|
|
dd0c17a9d4 | ||
|
|
d334726991 | ||
|
|
9aea4e5677 | ||
|
|
ffd9efeb98 | ||
|
|
5bf3bd7ce5 | ||
|
|
693fce1bb1 | ||
|
|
48fbb1076f | ||
|
|
7bc994b99f | ||
|
|
8586d1cc12 | ||
|
|
de280c23ea | ||
|
|
06887003cf | ||
|
|
e2eacbc051 | ||
|
|
93b81493c0 | ||
|
|
4287f60ab3 | ||
|
|
f62e9baa4a | ||
|
|
2d2da6e939 | ||
|
|
2bf3d5372b | ||
|
|
a4f8e20293 | ||
|
|
ac915e8973 | ||
|
|
e19a521c4e | ||
|
|
bafea756ee | ||
|
|
1bdfc8a5d2 | ||
|
|
6e6285c5ae | ||
|
|
f49411b3a9 | ||
|
|
1d3dee1950 | ||
|
|
caf3ddd68a | ||
|
|
17b5168bde | ||
|
|
9bfc16a4d5 | ||
|
|
98f5245317 | ||
|
|
4286c528f5 | ||
|
|
8f5f9d6238 | ||
|
|
26d3a53d44 | ||
|
|
de8e444aba | ||
|
|
35da556697 | ||
|
|
52ac5e5424 | ||
|
|
517206364a | ||
|
|
5e82eea9f1 | ||
|
|
b6e8768767 | ||
|
|
5df3d4f63b | ||
|
|
c59e9fc3f5 | ||
|
|
febe9871b9 | ||
|
|
4d5d881af6 | ||
|
|
2735deff73 | ||
|
|
ee93add6fc | ||
|
|
8ffab2ae70 | ||
|
|
fa216dddd5 | ||
|
|
34ec39a858 | ||
|
|
7dd611b61a | ||
|
|
5d017258b7 | ||
|
|
e58c00ecdf | ||
|
|
b88848ba8a | ||
|
|
6b6e7c647a | ||
|
|
b972422cc6 | ||
|
|
52ded23315 | ||
|
|
b3399f419e | ||
|
|
e2d60dde43 | ||
|
|
214b3c75d0 | ||
|
|
95659839b2 | ||
|
|
cab3f57737 | ||
|
|
dbbd01a83d | ||
|
|
90e66841df | ||
|
|
5ba41c6ea0 | ||
|
|
63c0de42db | ||
|
|
5646a22245 | ||
|
|
0e13801a7a | ||
|
|
7b0f7d639a | ||
|
|
301766f4aa | ||
|
|
6bdbfa2657 | ||
|
|
f5dafe6d43 | ||
|
|
623f25e521 | ||
|
|
a0e8a7f935 | ||
|
|
61bd31e421 | ||
|
|
84425e1319 | ||
|
|
09615f8ec4 | ||
|
|
3d368482bb | ||
|
|
df2cec5a94 | ||
|
|
369512c6c1 | ||
|
|
10a4cc270a | ||
|
|
1eda5e5860 | ||
|
|
3fc0d9418f | ||
|
|
c4f243f4b0 | ||
|
|
e057d740d8 | ||
|
|
a20c5677b4 | ||
|
|
7f687051f8 | ||
|
|
450c5c9829 | ||
|
|
0ab019e5ab | ||
|
|
40b3fa91ac | ||
|
|
f59c8da000 | ||
|
|
3555d01b68 | ||
|
|
b201b32b46 | ||
|
|
89f79e0977 | ||
|
|
aa0233f49b | ||
|
|
df4a45afb2 | ||
|
|
def0144dab | ||
|
|
ce16fa154c | ||
|
|
063a21997b | ||
|
|
92fce802c5 | ||
|
|
f76d597cbf | ||
|
|
ecbbbc2d6b | ||
|
|
2bb00a177e | ||
|
|
e13b2d4edc | ||
|
|
bb3a94eb84 | ||
|
|
49b460eee3 | ||
|
|
28da1a0ec9 | ||
|
|
6176e6ff40 | ||
|
|
4406287a5f | ||
|
|
3f63f289ed | ||
|
|
5c07dd3023 | ||
|
|
fe71591200 | ||
|
|
3f87e23bd7 | ||
|
|
9d2d99b5ba | ||
|
|
76df43fe66 | ||
|
|
0d72f6b987 | ||
|
|
507b6b9133 | ||
|
|
4911d61d78 | ||
|
|
bdf5f82959 | ||
|
|
86310083f2 | ||
|
|
29faa4932d | ||
|
|
f498bdabb3 | ||
|
|
f4a1f06ba6 | ||
|
|
317b0fc4d7 | ||
|
|
5d081f27d1 | ||
|
|
9ca126f3cd | ||
|
|
736d447d9e | ||
|
|
f2f27ac639 | ||
|
|
90f6fd83ba | ||
|
|
d9e9465415 | ||
|
|
f73fc49ee4 | ||
|
|
afe448647c | ||
|
|
091242bedc | ||
|
|
8d4f76156b | ||
|
|
50eacba438 | ||
|
|
5273dfbbf3 | ||
|
|
db5632d3ba | ||
|
|
9898a8424a | ||
|
|
0f354cfe9b | ||
|
|
b433736241 | ||
|
|
cee48c1f45 | ||
|
|
6d1ea96e79 | ||
|
|
5053f3f0f8 | ||
|
|
7b16c4ebe1 | ||
|
|
b8636029c9 | ||
|
|
357158db3e | ||
|
|
796ee5516f | ||
|
|
c82c1c192f | ||
|
|
c20cacac99 | ||
|
|
decfc5823d | ||
|
|
6da6377bb5 | ||
|
|
eedb46e000 | ||
|
|
fe8466b0e3 | ||
|
|
f10fbc3fa3 | ||
|
|
e186620a60 | ||
|
|
e1316ea48b | ||
|
|
a9adb3bb06 | ||
|
|
5ccab1770b | ||
|
|
3d377b7b1f | ||
|
|
8bdb578b90 | ||
|
|
50a9ea628d | ||
|
|
637ce5c1bc | ||
|
|
7432f4de08 | ||
|
|
594267b936 | ||
|
|
e210eca290 | ||
|
|
189ce3db64 | ||
|
|
07028ad3cd | ||
|
|
0d71781071 | ||
|
|
8e72efdd96 | ||
|
|
e0a656e2ba | ||
|
|
2afab43281 | ||
|
|
d1a09260d5 | ||
|
|
926db1c2b0 | ||
|
|
12dd676e9d | ||
|
|
dac72eec20 | ||
|
|
5e4d3c40f5 | ||
|
|
990237f975 | ||
|
|
4e29d5cbea | ||
|
|
9d58dc20cb | ||
|
|
b6bf77956b | ||
|
|
ae9e520df1 | ||
|
|
ae65c1af3d | ||
|
|
6f892c7dbf | ||
|
|
0c4fad6f54 | ||
|
|
1a9b749017 | ||
|
|
aee81cb824 | ||
|
|
6d6608754d | ||
|
|
326bfc035f | ||
|
|
3ff23e1c47 | ||
|
|
ada82fa7e1 | ||
|
|
51e4265109 | ||
|
|
eaebb3adfa | ||
|
|
76b4ca6ea5 | ||
|
|
b8560c6e15 | ||
|
|
8eec2549a4 | ||
|
|
aaa1ad7eaa | ||
|
|
74c57fc11c | ||
|
|
b7e25c4e1c | ||
|
|
3f97333423 | ||
|
|
71152f64a5 | ||
|
|
531d125dcc | ||
|
|
299e79e890 | ||
|
|
14f60d8dd3 | ||
|
|
a00b1a96c7 | ||
|
|
2847e02c5d | ||
|
|
57b30a1838 | ||
|
|
f2a637ada7 | ||
|
|
b3804586dc | ||
|
|
113c1f0565 | ||
|
|
3948eeaad4 | ||
|
|
f843184970 | ||
|
|
2aa9edeb8f | ||
|
|
5e37cf8224 | ||
|
|
8bd3a1edde | ||
|
|
c68962e2f2 | ||
|
|
6a9a247a87 | ||
|
|
e238370bc8 | ||
|
|
178f2fd135 | ||
|
|
55cd1160db | ||
|
|
06bdbd929c | ||
|
|
100eb01f27 | ||
|
|
00ade4ad29 | ||
|
|
f3a5779102 | ||
|
|
6f0b97e760 | ||
|
|
28f9ed64ff | ||
|
|
b6924f70d4 | ||
|
|
79ecae34c8 | ||
|
|
f4843baa0e | ||
|
|
cd97a275fe | ||
|
|
f2e77a5c3a | ||
|
|
23f4f9bd0b | ||
|
|
e8eb6964db | ||
|
|
9643c5bfcd | ||
|
|
56ebe6bb8b | ||
|
|
5f7c2f07b8 | ||
|
|
bbe965e7a3 | ||
|
|
ebd03c7b1a | ||
|
|
0d02a57600 | ||
|
|
394d3a5b25 | ||
|
|
4f9589fb6b | ||
|
|
048e3396de | ||
|
|
e3d378a4ba | ||
|
|
91f8d9c2b0 | ||
|
|
2285c896d2 | ||
|
|
9ff24918e2 | ||
|
|
e78e794240 | ||
|
|
0eb359542b | ||
|
|
c54cf5a2ba | ||
|
|
6f7f05b456 | ||
|
|
da6a8834dd | ||
|
|
7ed16d63f3 | ||
|
|
d8730cc67b | ||
|
|
b67efe0cac | ||
|
|
835cb2f90f | ||
|
|
f75e99f703 | ||
|
|
7e1bf0fc92 | ||
|
|
82693a8587 | ||
|
|
99eace0336 | ||
|
|
d0257fea8f | ||
|
|
453a5cda2e | ||
|
|
bdbd1a0f08 | ||
|
|
d32b9f78a4 | ||
|
|
46c2d8c49e | ||
|
|
88b5567131 | ||
|
|
0e81d16feb | ||
|
|
a900f0ff65 | ||
|
|
d4faa75b79 | ||
|
|
47b0de1b49 | ||
|
|
fe6a10592b | ||
|
|
8f02349ca5 | ||
|
|
1fb32de428 | ||
|
|
31b66ee6fa | ||
|
|
d22f9b81b2 | ||
|
|
3051d61365 | ||
|
|
53fd2e48e5 | ||
|
|
ef516c123d | ||
|
|
add8962390 | ||
|
|
1a444a481e | ||
|
|
96ca4a7933 | ||
|
|
f07bc615b0 | ||
|
|
2d0c88390a | ||
|
|
52574623fe | ||
|
|
b1d95ddf5f | ||
|
|
092bb285ed | ||
|
|
52ce26796a | ||
|
|
6bd55e4807 | ||
|
|
8325a9297e | ||
|
|
9cb3c78def | ||
|
|
143436ecd3 | ||
|
|
83cf87c8cf | ||
|
|
2f1003e8bc | ||
|
|
e0649eb9bd | ||
|
|
d5c607d370 | ||
|
|
2c2c8bf4cb | ||
|
|
08459ca289 | ||
|
|
162f1fa1d1 | ||
|
|
d9113edbbe | ||
|
|
eb1c5c07e9 | ||
|
|
0574429a93 | ||
|
|
8ff565404b | ||
|
|
b08d4b4cae | ||
|
|
f331662263 | ||
|
|
10693283c5 | ||
|
|
f2a933c51b | ||
|
|
172b8cfb9b | ||
|
|
47738ffa26 | ||
|
|
c8a29b4651 | ||
|
|
c305cc2f7d | ||
|
|
01676e1f99 | ||
|
|
de1e15fb25 | ||
|
|
0aa77c33ad | ||
|
|
22eeb1a47a | ||
|
|
15b3c6617b | ||
|
|
e342a88f7d | ||
|
|
7779efedf1 | ||
|
|
137b4d25b9 | ||
|
|
9be40adabf | ||
|
|
b6d4b7b40d | ||
|
|
3256789082 | ||
|
|
87adc575ac | ||
|
|
30ae0f93f3 | ||
|
|
c339654112 | ||
|
|
25c9770fba | ||
|
|
0bf7326604 | ||
|
|
7fc5a9ba8f | ||
|
|
230f805cae | ||
|
|
bc9c828584 | ||
|
|
6c17a709bf | ||
|
|
dd912ecc8f | ||
|
|
c9b12c4892 | ||
|
|
414aea2637 | ||
|
|
b7bc4dbadc | ||
|
|
8a8b871908 | ||
|
|
1225b27510 | ||
|
|
f86bd86043 | ||
|
|
8133164f44 | ||
|
|
2f447da34b | ||
|
|
41cdc8783e | ||
|
|
ccbb324270 | ||
|
|
8d4a0c34e7 | ||
|
|
2620f75955 | ||
|
|
4ca525f701 | ||
|
|
53a9aa377a | ||
|
|
d166003dfe | ||
|
|
31d80791a9 | ||
|
|
02245aa5f1 | ||
|
|
583b6b9a55 | ||
|
|
d4815e855e | ||
|
|
113665139a | ||
|
|
37e35b0ed6 | ||
|
|
eef8f00d8b | ||
|
|
bdb15a7780 | ||
|
|
2b59912549 | ||
|
|
8623085c2a | ||
|
|
612627d66f | ||
|
|
04700beab3 | ||
|
|
0d3d6c88e4 | ||
|
|
db57f67e98 | ||
|
|
baa145e15c | ||
|
|
c1bae20f01 | ||
|
|
41bc676b12 | ||
|
|
cea74dd63d | ||
|
|
9ccca3b33b | ||
|
|
1606cbab18 | ||
|
|
c428c8ae2d | ||
|
|
b52e84d1e7 | ||
|
|
3fbcbbe8e0 | ||
|
|
74eabd700c | ||
|
|
d7ddae38e5 | ||
|
|
fe7de2db99 | ||
|
|
9b9de42139 | ||
|
|
7b64288765 | ||
|
|
c6c967f89c | ||
|
|
040dba961a | ||
|
|
0146794cda | ||
|
|
b00eba77f9 | ||
|
|
3f27ae249a | ||
|
|
4384cffabe | ||
|
|
fde3234fc6 | ||
|
|
d998c7cd46 | ||
|
|
a8b62fc184 | ||
|
|
65de8e08d3 | ||
|
|
73cc1cd2e5 | ||
|
|
26926071b0 | ||
|
|
5c433129ec | ||
|
|
68bc1a6608 | ||
|
|
e67a0e0bc0 | ||
|
|
f553d45396 | ||
|
|
090a53b95a | ||
|
|
22ab7515fb | ||
|
|
a97dcc5efb | ||
|
|
4b098aef69 | ||
|
|
cb46274288 | ||
|
|
1e60a43075 | ||
|
|
1aa9cd032f | ||
|
|
5e3748315c | ||
|
|
7f67c032d1 | ||
|
|
33621af3ad | ||
|
|
2fb4a7bd6a | ||
|
|
1ff983a03b | ||
|
|
701530ef04 | ||
|
|
719e624ddf | ||
|
|
03a25df367 | ||
|
|
c93ce684a8 | ||
|
|
090708f38b | ||
|
|
49f93e660e | ||
|
|
867680f4e9 | ||
|
|
36ac529cc8 | ||
|
|
0ae388f0f6 | ||
|
|
b53ace765c | ||
|
|
f256299a0c | ||
|
|
4fb05e406a | ||
|
|
035639c68a | ||
|
|
09f53cac92 | ||
|
|
05a27d9003 | ||
|
|
5d4d1807ba | ||
|
|
836e465414 | ||
|
|
94457b24cd | ||
|
|
728aaec24d | ||
|
|
4ccd2d7087 | ||
|
|
9bc6d2928b | ||
|
|
d7d4fb36ad | ||
|
|
4e6a88abec | ||
|
|
a7051ace5d | ||
|
|
cee47f2846 | ||
|
|
f9e60acdf6 | ||
|
|
5f6ba048e6 | ||
|
|
aba91de480 | ||
|
|
118ec757a3 | ||
|
|
eaf712937b | ||
|
|
6c47c406cb | ||
|
|
eaeff3bf6a | ||
|
|
5b5fb3e32d | ||
|
|
07a8dd4912 | ||
|
|
a2f5127222 | ||
|
|
eab48ea202 | ||
|
|
00e2717653 | ||
|
|
d83f150810 | ||
|
|
2a7e6c2dd2 | ||
|
|
f8f792645b | ||
|
|
8c79f39389 | ||
|
|
f7da653e87 | ||
|
|
1ed753eff1 | ||
|
|
385de967a4 | ||
|
|
ac84019777 | ||
|
|
b0c6fe9474 | ||
|
|
94e2b0233a | ||
|
|
c202e3426b | ||
|
|
7e86e83173 | ||
|
|
604857648b | ||
|
|
76fb50473f | ||
|
|
ffee3b225f | ||
|
|
5f1dd42559 | ||
|
|
57a7c78ce2 | ||
|
|
33c6643388 | ||
|
|
74deba61e6 | ||
|
|
bb2f0e6c55 | ||
|
|
b2f6f68d1a | ||
|
|
fdde065a94 | ||
|
|
9231b0914b | ||
|
|
78f49ae7c9 | ||
|
|
95ff81d81f | ||
|
|
92b73f62ad | ||
|
|
1057836788 | ||
|
|
93f8702962 | ||
|
|
3d71454666 | ||
|
|
c0cc2744c6 | ||
|
|
65b1c34d06 | ||
|
|
67615719ae | ||
|
|
491c8ebe77 | ||
|
|
cae23d61e2 | ||
|
|
9cad9ad2f9 | ||
|
|
ba977d6b49 | ||
|
|
88bb2f91a5 | ||
|
|
dda11627f8 | ||
|
|
2df713ca0c | ||
|
|
486ede9396 | ||
|
|
72afcc3e5a | ||
|
|
ef2eaceb88 | ||
|
|
c2db1c70e3 | ||
|
|
13413899eb | ||
|
|
98f074e8d5 | ||
|
|
36a60bae4d | ||
|
|
7c5a5daf2f | ||
|
|
7b74df7f04 | ||
|
|
a6813a7ec1 | ||
|
|
484d7ee3b2 | ||
|
|
7a1bc282e9 | ||
|
|
f45480c0b9 | ||
|
|
23d980910d | ||
|
|
9b939aa8b4 | ||
|
|
838a9c3e7d | ||
|
|
f91de30717 | ||
|
|
0c3299573f | ||
|
|
13e2d2d597 | ||
|
|
a6bbc7119d | ||
|
|
20bf58b84f | ||
|
|
fe176c92c0 | ||
|
|
6009ed53b7 | ||
|
|
79c387ba45 | ||
|
|
dd767d6664 | ||
|
|
61202f1635 | ||
|
|
457d6cc54b | ||
|
|
bf8123b7c5 | ||
|
|
c98ede9b81 | ||
|
|
34671fca59 | ||
|
|
7ad9381aad | ||
|
|
7387eefb10 | ||
|
|
ca3403a5b2 | ||
|
|
8adc895c8c | ||
|
|
6fe49d06ac | ||
|
|
c47e6d0219 | ||
|
|
99c4b240ec | ||
|
|
0947724245 | ||
|
|
15653ef961 | ||
|
|
893c92266a | ||
|
|
50571995a4 | ||
|
|
dc2f6e17cd | ||
|
|
9a861b085a | ||
|
|
714cb318a9 | ||
|
|
2827463b41 | ||
|
|
121a8b11b8 | ||
|
|
3e0e31a5fe | ||
|
|
a7c96f77fe | ||
|
|
d3444e9aa0 | ||
|
|
71d2e61551 | ||
|
|
3756d1185f | ||
|
|
5a22eb9542 | ||
|
|
cb535f1cee | ||
|
|
f304e5efb7 | ||
|
|
5732a8ffc8 | ||
|
|
8b7ccb837a | ||
|
|
f4bb3fc0bc | ||
|
|
62602a88bc | ||
|
|
f63fa9faf2 | ||
|
|
839bfd4756 | ||
|
|
9392d21783 | ||
|
|
ca5594dfbb | ||
|
|
1b938417e5 | ||
|
|
f95e94ac6d | ||
|
|
e9bb72a416 | ||
|
|
2d003febc1 | ||
|
|
42c27af604 | ||
|
|
ee490fc660 | ||
|
|
10500c2fba | ||
|
|
c4011c0817 | ||
|
|
bc49bebb58 | ||
|
|
dcdd93bcaa | ||
|
|
d5fdc3ce63 | ||
|
|
c1b5a8625a | ||
|
|
dd1fd34865 | ||
|
|
9b9e65abd4 | ||
|
|
e5f917a185 | ||
|
|
368bf4a6d9 | ||
|
|
a7df7a5436 | ||
|
|
d3f5236073 | ||
|
|
db6fdbc121 | ||
|
|
dc9aa4d6b4 | ||
|
|
5f2553047d | ||
|
|
d556f84929 | ||
|
|
4345ccf356 | ||
|
|
4032468e10 | ||
|
|
5330f0f8e2 | ||
|
|
39e6ecc920 | ||
|
|
928e8f14ea | ||
|
|
0e4ffcee62 | ||
|
|
955f353a31 | ||
|
|
44df06fac7 | ||
|
|
f099f6f4a9 | ||
|
|
e9259356bd | ||
|
|
2264445772 | ||
|
|
47f0e1f1bf | ||
|
|
79d0898fb5 | ||
|
|
85d1119ad6 | ||
|
|
52e73ca7cd | ||
|
|
b5ac42792e | ||
|
|
6a844e163b | ||
|
|
efa4151850 | ||
|
|
55dc387d3c | ||
|
|
ae47333eef | ||
|
|
5bbe4cde77 | ||
|
|
0ff35d13e8 | ||
|
|
3059ab835b | ||
|
|
798f295912 | ||
|
|
71d115d161 | ||
|
|
d5368e5f90 | ||
|
|
d1f0b2e459 | ||
|
|
ed4ddc0a43 | ||
|
|
61c09a5b49 | ||
|
|
a6194f2535 | ||
|
|
901116fde6 | ||
|
|
d829525ab8 | ||
|
|
ee666a3a6f | ||
|
|
4a8a5829d7 | ||
|
|
fd2271f6d6 | ||
|
|
cac36727e8 | ||
|
|
e6bd0d0006 | ||
|
|
4ef83a844f | ||
|
|
23dc46d697 | ||
|
|
6fc502b0b2 | ||
|
|
a4f22a8542 | ||
|
|
bce7a1ad9a | ||
|
|
49a6e4633a | ||
|
|
41c02cb732 | ||
|
|
0f00111373 | ||
|
|
6521d24925 | ||
|
|
34468198ec | ||
|
|
ab30aae08e | ||
|
|
eb4d8cde7c | ||
|
|
95daa4b02e | ||
|
|
7e7331fb6f | ||
|
|
7485f00f2f | ||
|
|
c3704fa4ff | ||
|
|
db39e79cf7 | ||
|
|
7c8ffec254 | ||
|
|
7e3d5d17f8 | ||
|
|
bbfe228e4c | ||
|
|
a15e04c7d0 | ||
|
|
301b4de31d | ||
|
|
73f48b71a2 | ||
|
|
d7aa24b467 | ||
|
|
377dee7d83 | ||
|
|
079ec94197 | ||
|
|
aee0905ea5 | ||
|
|
5bc32ea89a | ||
|
|
a6162b8440 | ||
|
|
3f389fcf79 | ||
|
|
d78716c873 | ||
|
|
aabbf882df | ||
|
|
6b1b9252b3 | ||
|
|
e26fd5ed6f | ||
|
|
249cfa21e3 | ||
|
|
fb4d6afb07 | ||
|
|
909ffc1aac | ||
|
|
9ec5bd7183 | ||
|
|
dbde3de8e8 | ||
|
|
5c80f3197a | ||
|
|
17a644c5d8 | ||
|
|
9bf420d317 | ||
|
|
be2911707c | ||
|
|
f601044645 | ||
|
|
c55a83cbe2 | ||
|
|
ef9326abc6 | ||
|
|
a219c1b570 | ||
|
|
1920a3e802 | ||
|
|
67d0809e2f | ||
|
|
7c6676c981 | ||
|
|
519f2ba617 | ||
|
|
0b23fad909 | ||
|
|
6fe61a041e | ||
|
|
fb323c2086 | ||
|
|
dccb9bb49c | ||
|
|
e9505f31f7 | ||
|
|
c31e880f63 | ||
|
|
b064e73ef4 | ||
|
|
606c2c6f95 | ||
|
|
c0e02726be | ||
|
|
2ddf2e8341 | ||
|
|
5d2e647f10 | ||
|
|
a141122fbd | ||
|
|
b186677a53 | ||
|
|
c8ba9ef99a | ||
|
|
c994638168 | ||
|
|
b6df64337d | ||
|
|
1c0936d4c1 | ||
|
|
983671f5c3 | ||
|
|
d6f76de349 | ||
|
|
7aede20819 | ||
|
|
0e5c7e1215 | ||
|
|
3cd60e9a8d | ||
|
|
cf79e95c45 | ||
|
|
3c22cd216c | ||
|
|
cb20075599 | ||
|
|
c1257afc4c | ||
|
|
0862116a40 | ||
|
|
6eefc0a623 | ||
|
|
ed4f448370 | ||
|
|
3dc4f4641c | ||
|
|
95a5f5063e | ||
|
|
4cca167fc1 | ||
|
|
301ebd9b6f | ||
|
|
01fe0ca381 | ||
|
|
b6a9f5109c | ||
|
|
3320e993e7 | ||
|
|
239672116b | ||
|
|
4d8e71fff4 | ||
|
|
4babf6d30e | ||
|
|
9fe1808e1c | ||
|
|
ebb9dbc7f1 | ||
|
|
e2dc5e0705 | ||
|
|
f66f464199 | ||
|
|
35d42b9ec5 | ||
|
|
0776551aa7 | ||
|
|
482a890447 | ||
|
|
bc8ebd8db3 | ||
|
|
521a81ca1d | ||
|
|
c4a7b1f129 | ||
|
|
d712eb3a59 | ||
|
|
21cd0cb346 | ||
|
|
b1a0dfa7bd | ||
|
|
ffbedc1e02 | ||
|
|
0a647264d7 | ||
|
|
1385e28911 | ||
|
|
2ab843efdb | ||
|
|
90dac7dd19 | ||
|
|
792f7d8e21 | ||
|
|
5b0eb07d0d | ||
|
|
8abfe93df7 | ||
|
|
3ad6cdef51 | ||
|
|
0da2ad0c10 | ||
|
|
84faabca86 | ||
|
|
49015e3422 | ||
|
|
906f0876a0 | ||
|
|
e18cd87a07 | ||
|
|
35aed5fb55 | ||
|
|
6089e5fe20 | ||
|
|
2a316ebb1e | ||
|
|
1f83fe1b48 | ||
|
|
7d1bb4af5b | ||
|
|
f57fea0a54 | ||
|
|
13787708e9 | ||
|
|
7af3bc519a | ||
|
|
e3d96de203 | ||
|
|
ec08177607 | ||
|
|
362dc9bbcc | ||
|
|
a9fb0b3b08 | ||
|
|
c6b417ca81 | ||
|
|
4e64af0f10 | ||
|
|
2a2d99191d | ||
|
|
b0b6e71d2e | ||
|
|
041904f4c6 | ||
|
|
5b2bf8956a | ||
|
|
a1278fad49 | ||
|
|
d84b831922 | ||
|
|
fe37d4e91c | ||
|
|
f3a08df532 | ||
|
|
f9b0f6d613 | ||
|
|
b72e74e0d3 |
@@ -1,7 +1,9 @@
|
||||
node_modules
|
||||
bower_components
|
||||
.vagrant
|
||||
results/*
|
||||
package-lock.json
|
||||
.tmp
|
||||
tmp
|
||||
coverage
|
||||
|
||||
har.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
har.json
|
||||
.idea/
|
||||
@@ -1,8 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
before_install:
|
||||
- "npm install -g grunt-cli"
|
||||
- "npm install -g phantomjs"
|
||||
install: npm install
|
||||
before_script: grunt build
|
||||
@@ -0,0 +1 @@
|
||||
ko_fi: gaelmetais
|
||||
@@ -1,105 +0,0 @@
|
||||
module.exports = function(grunt) {
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
||||
font: {
|
||||
icons: {
|
||||
src: ['app/public/fonts/svg-icons/*.svg'],
|
||||
destCss: 'app/public/styles/less/icons.less',
|
||||
destFonts: 'app/public/fonts/icons.woff',
|
||||
|
||||
// Optional: Custom routing of font filepaths for CSS
|
||||
cssRouter: function (fontpath) {
|
||||
var pathArray = fontpath.split('/');
|
||||
var fileName = pathArray[pathArray.length - 1];
|
||||
return '/public/fonts/' + fileName;
|
||||
}
|
||||
}
|
||||
},
|
||||
less: {
|
||||
all: {
|
||||
files: {
|
||||
'app/public/styles/main.css': [ 'app/public/styles/less/main.less' ],
|
||||
'app/public/styles/index.css': [ 'app/public/styles/less/index.less' ],
|
||||
'app/public/styles/launchTest.css': [ 'app/public/styles/less/launchTest.less' ],
|
||||
'app/public/styles/results.css': [ 'app/public/styles/less/results.less' ]
|
||||
}
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
all: [
|
||||
'*.js',
|
||||
'app/lib/*',
|
||||
'app/nodeControllers/*.js',
|
||||
'app/public/scripts/*.js',
|
||||
'phantomas_custom/**/*.js'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
icons: {
|
||||
src: ['tmp']
|
||||
},
|
||||
coverage: {
|
||||
src: ['coverage/']
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
coverage: {
|
||||
src: ['test/**'],
|
||||
dest: 'coverage/'
|
||||
}
|
||||
},
|
||||
blanket: {
|
||||
coverage: {
|
||||
src: ['app/'],
|
||||
dest: 'coverage/app/'
|
||||
}
|
||||
},
|
||||
mochaTest: {
|
||||
test: {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['coverage/test/server/*.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
reporter: 'html-cov',
|
||||
// use the quiet flag to suppress the mocha console output
|
||||
quiet: true,
|
||||
// specify a destination file to capture the mocha
|
||||
// output (the quiet option does not suppress this)
|
||||
captureFile: 'coverage/coverage.html'
|
||||
},
|
||||
src: ['coverage/test/server/*.js']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
|
||||
|
||||
grunt.registerTask('icons', [
|
||||
'font:icons',
|
||||
'less',
|
||||
'clean:icons'
|
||||
]);
|
||||
|
||||
grunt.registerTask('build', [
|
||||
'jshint',
|
||||
'less'
|
||||
]);
|
||||
|
||||
grunt.registerTask('hint', [
|
||||
'jshint'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
'clean:coverage',
|
||||
'blanket',
|
||||
'copy:coverage',
|
||||
'mochaTest'
|
||||
]);
|
||||
|
||||
};
|
||||
@@ -1,37 +1,91 @@
|
||||
# Yellow Lab Tools
|
||||
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
|
||||
|
||||
Online tool designed to help Front-End developers optimize their website
|
||||
|
||||
[](https://gemnasium.com/gmetais/YellowLabTools) [](https://travis-ci.org/gmetais/YellowLabTools)
|
||||
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
|
||||
|
||||
|
||||
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
|
||||
#
|
||||
|
||||
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>online tool</b> (recommended):
|
||||
<a href="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/home.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>Docker image</b> - <a href="https://github.com/ousamabenyounes/docker-yellowlabtools" target="_blank">ousamabenyounes/docker-yellowlabtools</a>
|
||||
<br>
|
||||
Your own private instance of Yellow Lab Tools, on your computer.
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/docker-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-cli-animated.gif"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>NodeJS</b> package - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/npm-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>public API</b> - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
|
||||
<br>
|
||||
Allows you to launch runs from any language
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/api-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The tool is based on the fabulous [Phantomas](https://github.com/macbre/phantomas) by Maciej Brencz, that loads a page and collects many web performance metrics.
|
||||
I rewrote some modules to deeper analyze Javascript interactions with the DOM (especially with jQuery).
|
||||
And then it is wrapped inside a small NodeJS server to get an interface (inspired by my favorite tool: [WebPageTest](http://www.webpagetest.org/)).
|
||||
The tool loads the given URL and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
|
||||
|
||||
**There are so many things left to do, your help would be greatly appreciated! Please report bugs, ask for evolutions and come code with me.**
|
||||
By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!!
|
||||
|
||||

|
||||
|
||||
|
||||
## Help needed
|
||||
## Install your own private instance
|
||||
|
||||
I'm currently looking for help in correcting the english wordings in the tool, as it is not my native language.
|
||||
If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
|
||||
|
||||
The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
|
||||
|
||||
## Install your own instance
|
||||
|
||||
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-own-instance).
|
||||
|
||||
|
||||
## License
|
||||
Please read the [license](LICENSE).
|
||||
|
||||
Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
||||
The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
|
||||
|
||||
|
||||
## Author
|
||||
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
|
||||
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of Yellow Lab Tools!
|
||||
|
||||
<a href='https://ko-fi.com/gaelmetais' target='_blank'><img height='35' style='border:0px;height:46px;' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy me a coffee' /><a>
|
||||
|
||||
I can also help your company about Web Performances, visit [my website](https://www.gaelmetais.com).
|
||||
|
||||
|
||||
## Contributors
|
||||
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
|
||||
- Km [camlafit](https://github.com/camlafit)
|
||||
- Vincent L. [magikcypress](https://github.com/magikcypress)
|
||||
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
Vagrant.configure("2") do |config|
|
||||
|
||||
config.vm.box = "precise64"
|
||||
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
|
||||
|
||||
config.vm.network :private_network, ip: "10.10.10.10"
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
vb.customize ["modifyvm", :id, "--memory", 512]
|
||||
vb.customize ["modifyvm", :id, "--cpus", 2]
|
||||
end
|
||||
|
||||
config.vm.provision :shell, :path => "server_config/server_install.sh"
|
||||
|
||||
end
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Yellow Lab Tools main file
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var phantomas = require('phantomas');
|
||||
|
||||
var PhantomasWrapper = function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This is the phantomas launcher. It merges user chosen options into the default options
|
||||
* Available options :
|
||||
*
|
||||
* - timeout : in seconds (default 60)
|
||||
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree (reported durations of main tasks will be slower than usual)
|
||||
*
|
||||
*/
|
||||
this.execute = function(task, callback) {
|
||||
|
||||
var options = {
|
||||
// Cusomizable options
|
||||
timeout: task.options.timeout || 60,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
|
||||
// Mandatory
|
||||
reporter: 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
'domComplexity', // overriden
|
||||
'domMutations', // not compatible with webkit
|
||||
'domQueries', // overriden
|
||||
'eventListeners', // overridden
|
||||
'filmStrip', // not needed
|
||||
'har', // not needed for the moment
|
||||
'pageSource', // not needed
|
||||
'screenshot', // not needed for the moment
|
||||
'waitForSelector', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
'include-dirs': [
|
||||
'phantomas_custom/core',
|
||||
'phantomas_custom/modules'
|
||||
].join(',')
|
||||
};
|
||||
|
||||
// Output the command line for debugging purpose
|
||||
console.log('If you want to reproduce the phantomas task only, copy the following command line:');
|
||||
var optionsString = '';
|
||||
for (var opt in options) {
|
||||
optionsString += ' ' + '--' + opt + '=' + options[opt];
|
||||
}
|
||||
console.log('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
|
||||
|
||||
// Kill the application if nothing happens for 10 minutes
|
||||
var killer = setTimeout(function() {
|
||||
console.log('Killing the server because the test ' + task.testId + ' on ' + task.url + ' was launched 10 minutes ago');
|
||||
// Forever will restart the server
|
||||
process.exit(1);
|
||||
}, 600000);
|
||||
|
||||
// It's time to launch the test!!!
|
||||
var triesNumber = 3;
|
||||
|
||||
async.retry(triesNumber, function(cb) {
|
||||
phantomas(task.url, options, function(err, json, results) {
|
||||
console.log('Returning from Phantomas');
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
|
||||
err = 1001;
|
||||
}
|
||||
|
||||
if (!err && (!json || !json.metrics)) {
|
||||
err = 1002;
|
||||
}
|
||||
|
||||
// Don't cancel test if it is a timeout and we've got some results
|
||||
if (err === 252 && json) {
|
||||
console.log('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
|
||||
err = null;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
console.log('Attempt failed for test id ' + task.testId + '. Error code ' + err);
|
||||
}
|
||||
|
||||
cb(err, {json: json, results: results});
|
||||
});
|
||||
}, function(err, data) {
|
||||
|
||||
clearTimeout(killer);
|
||||
|
||||
if (err) {
|
||||
console.log('All ' + triesNumber + ' attemps failed for test id ' + task.testId);
|
||||
}
|
||||
callback(err, data.json, data.results);
|
||||
});
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new PhantomasWrapper();
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Alternative to the standard String.prototype.replace function
|
||||
* Avoids problems with $$, $1, $2, ...
|
||||
*/
|
||||
|
||||
module.exports = function(str, searched, replacement) {
|
||||
return str.split(searched).join(replacement);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Creation of a queue and it's worker function
|
||||
*/
|
||||
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var phantomasWrapper = require('./phantomasWrapper');
|
||||
|
||||
|
||||
var testQueue = function() {
|
||||
'use strict';
|
||||
|
||||
var currentTask = null;
|
||||
var self = this;
|
||||
|
||||
var queue = async.queue(function(task, callback) {
|
||||
currentTask = task;
|
||||
|
||||
console.log('Starting test ' + task.testId);
|
||||
|
||||
phantomasWrapper.execute(task, function(err, json, results) {
|
||||
console.log('Test ' + task.testId + ' complete');
|
||||
currentTask = null;
|
||||
callback(err, json, results);
|
||||
self.emit('queueMoving');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Use this method to add a test to the queue
|
||||
this.push = queue.push;
|
||||
|
||||
|
||||
// Gives the position of a task in the queue
|
||||
// Returns 0 if it is the current running task
|
||||
// Returns -1 if not found
|
||||
this.indexOf = function(testId) {
|
||||
if (currentTask && currentTask.testId === testId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var position = -1;
|
||||
if (queue.length() > 0) {
|
||||
queue.tasks.forEach(function(task, index) {
|
||||
if (task.data.testId === testId) {
|
||||
position = index + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
this.testComplete = function(testId) {
|
||||
self.emit('testComplete', testId);
|
||||
};
|
||||
|
||||
this.testFailed = function(testId) {
|
||||
self.emit('testFailed', testId);
|
||||
};
|
||||
};
|
||||
|
||||
// extend the EventEmitter class
|
||||
util.inherits(testQueue, EventEmitter);
|
||||
|
||||
module.exports = new testQueue();
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Yellow Lab Tools home page controller
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require ('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var indexController = function(req, res, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
async.parallel({
|
||||
|
||||
htmlTemplate: function(callback) {
|
||||
fs.readFile('./app/node_views/index.html', {encoding: 'utf8'}, callback);
|
||||
}
|
||||
|
||||
}, function(err, results) {
|
||||
var html = results.htmlTemplate;
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = indexController;
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Controller for the test launching page (the waiting page, after the user submited a test on the index page)
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require ('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var launchTestController = function(req, res, testQueue, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
// Generate test id
|
||||
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
|
||||
|
||||
var resultsPath = 'results/' + testId;
|
||||
var phantomasResultsPath = resultsPath + '/results.json';
|
||||
|
||||
var url = req.body.url;
|
||||
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
var options = {};
|
||||
if (req.body.timeout) {
|
||||
options.timeout = req.body.timeout;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
|
||||
function htmlTemplate(callback) {
|
||||
fs.readFile('./app/node_views/launchTest.html', {encoding: 'utf8'}, callback);
|
||||
},
|
||||
|
||||
function sendResponse(html, callback) {
|
||||
|
||||
html = strReplace(html, '%%TEST_URL%%', url);
|
||||
html = strReplace(html, '%%TEST_ID%%', testId);
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
function createFolder(callback) {
|
||||
// Create results folder
|
||||
fs.mkdir(resultsPath, callback);
|
||||
},
|
||||
|
||||
function executePhantomas(callback) {
|
||||
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
|
||||
|
||||
var task = {
|
||||
testId: testId,
|
||||
url: url,
|
||||
options: options
|
||||
};
|
||||
|
||||
testQueue.push(task, callback);
|
||||
},
|
||||
|
||||
function writeResults(json, resultsObject, callback) {
|
||||
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
|
||||
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
|
||||
}
|
||||
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.log('An error occured in the phantomas test: ', err);
|
||||
|
||||
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
|
||||
if (err) {
|
||||
console.log('Could not even write an error message on file ' + phantomasResultsPath);
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
testQueue.testFailed(testId);
|
||||
} else {
|
||||
testQueue.testComplete(testId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = launchTestController;
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* The page that dispays the results
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var resultsController = function(req, res, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
var testId = req.params.testId;
|
||||
var resultsPath = 'results/' + testId;
|
||||
var phantomasResultsPath = resultsPath + '/results.json';
|
||||
|
||||
console.log('Opening test ' + testId + ' results as HTML');
|
||||
|
||||
async.parallel({
|
||||
|
||||
htmlTemplate: function(callback) {
|
||||
fs.readFile('./app/node_views/results.html', {encoding: 'utf8'}, callback);
|
||||
},
|
||||
|
||||
phantomasResults: function(callback) {
|
||||
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
|
||||
}
|
||||
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return res.status(404).send('Sorry, test not found...');
|
||||
}
|
||||
|
||||
// Escape "</script>" because it can interfer with the HTML parser
|
||||
var phantomasResults = results.phantomasResults;
|
||||
phantomasResults = phantomasResults.replace(/<\/script>/g, '\\u003c/script>');
|
||||
|
||||
var html = results.htmlTemplate;
|
||||
html = strReplace(html, '%%RESULTS%%', phantomasResults);
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = resultsController;
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Socket.io handler
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var waitingQueueSocket = function(socket, testQueue) {
|
||||
|
||||
socket.on('waiting', function(testId) {
|
||||
console.log('User waiting for test id ' + testId);
|
||||
|
||||
sendTestStatus(testId);
|
||||
|
||||
testQueue.on('testComplete', function(id) {
|
||||
if (testId === id) {
|
||||
socket.emit('complete');
|
||||
console.log('Sending complete event to test id ' + testId);
|
||||
}
|
||||
});
|
||||
|
||||
testQueue.on('testFailed', function(id) {
|
||||
if (testId === id) {
|
||||
socket.emit('failed');
|
||||
console.log('Sending failed event to test id ' + testId);
|
||||
}
|
||||
});
|
||||
|
||||
testQueue.on('queueMoving', function() {
|
||||
var positionInQueue = testQueue.indexOf(testId);
|
||||
if (positionInQueue >= 0) {
|
||||
socket.emit('position', positionInQueue);
|
||||
console.log('Sending position to test id ' + testId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finds the status of a test and send it to the client
|
||||
function sendTestStatus(testId) {
|
||||
// Check task position in queue
|
||||
var positionInQueue = testQueue.indexOf(testId);
|
||||
|
||||
if (positionInQueue >= 0) {
|
||||
socket.emit('position', positionInQueue);
|
||||
} else {
|
||||
// Find in results files
|
||||
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
|
||||
if (exists) {
|
||||
// TODO : find a way to make sure the file is completely written
|
||||
setTimeout(function() {
|
||||
socket.emit('complete');
|
||||
}, 4000);
|
||||
} else {
|
||||
socket.emit('404');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = waitingQueueSocket;
|
||||
@@ -1,35 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
|
||||
<form method="post" action="/launchTest">
|
||||
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
|
||||
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
|
||||
</form>
|
||||
<p>(This is a BETA, your feedback is more than welcome)</p>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,97 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools - Awaiting</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<div>%%TEST_URL%%</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var testId = '%%TEST_ID%%';
|
||||
var statusElement = document.getElementById('status');
|
||||
var socket = io();
|
||||
|
||||
function askStatus() {
|
||||
socket.emit('waiting', testId);
|
||||
}
|
||||
|
||||
socket.on('position', function(position) {
|
||||
if (position === 0) {
|
||||
statusElement.innerHTML = 'Test is running...';
|
||||
} else {
|
||||
statusElement.innerHTML = 'Waiting behind ' + (position) + ' other tests';
|
||||
}
|
||||
sendPositionInQueueToGA(position);
|
||||
|
||||
// Rotate the spinner
|
||||
document.body.className = 'waiting';
|
||||
});
|
||||
|
||||
socket.on('complete', function() {
|
||||
statusElement.innerHTML = 'Test complete';
|
||||
window.location.replace('/results/' + testId);
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('failed', function() {
|
||||
statusElement.innerHTML = 'Test failed';
|
||||
window.location.replace('/results/' + testId);
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('404', function() {
|
||||
statusElement.innerHTML = 'Test not found';
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', function() {
|
||||
statusElement.innerHTML = "Server reboot. Please start a new test."
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
askStatus();
|
||||
|
||||
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
}
|
||||
|
||||
// Need stats on average queue position
|
||||
var positionAlreadySent = false;
|
||||
function sendPositionInQueueToGA(position) {
|
||||
if (!positionAlreadySent && GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
ga('send', 'pageview', {
|
||||
queuePosition: position
|
||||
});
|
||||
positionAlreadySent = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,933 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools - Results page</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/ngModal/dist/ng-modal.js"></script>
|
||||
<script src="/public/scripts/app.js"></script>
|
||||
<script src="/public/scripts/resultsCtrl.js"></script>
|
||||
<script src="/public/scripts/showOffenders.js"></script>
|
||||
</head>
|
||||
<body ng-app="YellowLabTools" ng-controller="ResultsCtrl">
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
|
||||
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
|
||||
|
||||
<div class="ng-cloak">
|
||||
<div>Tested url: <a class="testedUrl" href="{{phantomasResults.url}}" target="_blank">{{phantomasResults.url}}</a></div>
|
||||
|
||||
<div class="resultsMenu">
|
||||
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
|
||||
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'execution'}" ng-click="setView('execution')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
|
||||
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'summary'}" ng-click="setView('summary')"><div class="icon-summary"></div><span>Grades</span></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="phantomasResults.error">
|
||||
<h2>Error: {{phantomasResults.error}}</h2>
|
||||
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
|
||||
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
|
||||
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
|
||||
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
|
||||
<div ng-if="phantomasResults.error == 1001">Javascript execution tree error</div>
|
||||
<div ng-if="phantomasResults.error == 1002">JSON undefined error</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="view == 'summary' && !phantomasResults.error" class="summary">
|
||||
<h2>Grades</h2>
|
||||
|
||||
<div class="notations">
|
||||
<div>
|
||||
<div ng-class="notations.domComplexity">{{notations.domComplexity}}</div>
|
||||
<div class="notation">DOM complexity</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.DOMelementsCount" ng-class="{'warning': phantomasResults.metrics.DOMelementsCount > 5000}">
|
||||
<div class="label">DOM elements count</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMelementsCount}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementsCountTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementsCountTooltip" dialog-title="Avoid too many DOM elements">
|
||||
<p>A high number of DOM elements means a lot of work for the browser to render the page.</p>
|
||||
<p>It also slows down Javascript DOM queries, as there are more elements to search through.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMelementMaxDepth" ng-class="{'warning': phantomasResults.metrics.DOMelementMaxDepth > 30}">
|
||||
<div class="label">DOM max depth</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMelementMaxDepth}}
|
||||
<show-offenders modal-title="DOM max depth" metric-name="DOMelementMaxDepth" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="Reduce DOM depth">
|
||||
<p>A deep DOM makes the CSS matching with DOM elements difficult.</p>
|
||||
<p>It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.iframesCount" ng-class="{'warning': phantomasResults.metrics.iframesCount > 30}">
|
||||
<div class="label">Number of iframes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.iframesCount}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="iFrames are slow">
|
||||
<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMidDuplicated" ng-class="{'warning': phantomasResults.metrics.DOMidDuplicated > 20}">
|
||||
<div class="label">IDs duplicated</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMidDuplicated}}
|
||||
<show-offenders modal-title="DOM IDs duplicated" metric-name="DOMidDuplicated" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMidDuplicatedTooltip = true"></div>
|
||||
<modal-dialog show="DOMidDuplicatedTooltip" dialog-title="Warning: duplicated DOM IDs">
|
||||
<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jsDomManipulations">{{notations.jsDomManipulations}}</div>
|
||||
<div class="notation">DOM manipulations</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.DOMinserts" ng-class="{'warning': phantomasResults.metrics.DOMinserts > 1000}">
|
||||
<div class="label">DOM inserts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMinserts}}
|
||||
<show-offenders modal-title="DOM inserts" metric-name="DOMinserts" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMinsertsTooltip = true"></div>
|
||||
<modal-dialog show="DOMinsertsTooltip" dialog-title="DOM insertions are slow">
|
||||
<p>Working with the DOM in Javascript triggers layout calculations and slows down the page.</p>
|
||||
<p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMqueries" ng-class="{'warning': phantomasResults.metrics.DOMqueries > 2000}">
|
||||
<div class="label">DOM queries</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMqueries}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMqueriesTooltip = true"></div>
|
||||
<modal-dialog show="DOMqueriesTooltip" dialog-title="Avoid having too many DOM queries">
|
||||
<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p>
|
||||
<p>Try to reduce the number of queries by refactoring your Javascript code.</p>
|
||||
<p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMqueriesAvoidable" ng-class="{'warning': phantomasResults.metrics.DOMqueriesAvoidable > 500}">
|
||||
<div class="label">Avoidable queries</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMqueriesAvoidable}}
|
||||
<show-offenders modal-title="Duplicated DOM queries" metric-name="DOMqueriesDuplicated" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="duplicatedQueriesCountAllTooltip = true"></div>
|
||||
<modal-dialog show="duplicatedQueriesCountAllTooltip" dialog-title="Cache duplicated DOM queries">
|
||||
<p>This is the number of queries that could be avoided by removing all duplicated queries.</p>
|
||||
<p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.eventsBound" ng-class="{'warning': phantomasResults.metrics.eventsBound > 2000}">
|
||||
<div class="label">Events bound</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.eventsBound}}
|
||||
<show-offenders modal-title="Events bound" metric-name="eventsBound" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="eventsBoundTooltip = true"></div>
|
||||
<modal-dialog show="eventsBoundTooltip" dialog-title="Reduce the number of events binding">
|
||||
<p>Binding too many events has a cost.</p>
|
||||
<p>It can be avoided by using "event delegation". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jsBadPractices">{{notations.jsBadPractices}}</div>
|
||||
<div class="notation">Bad Javascript</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.jsErrors" ng-class="{'warning': phantomasResults.metrics.jsErrors > 5}">
|
||||
<div class="label">Javascript errors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsErrors}}
|
||||
<show-offenders modal-title="Javascript errors" metric-name="jsErrors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jsErrorsTooltip = true"></div>
|
||||
<modal-dialog show="jsErrorsTooltip" dialog-title="Javascript errors">
|
||||
<p>Just to let you know there are some errors on the page.</p>
|
||||
<p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.evalCalls">
|
||||
<div class="label">eval calls</div>
|
||||
<div class="result">{{phantomasResults.metrics.evalCalls}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="evalCallsTooltip = true"></div>
|
||||
<modal-dialog show="evalCallsTooltip" dialog-title="Eval is evil">
|
||||
<p>The 'eval' function is slow and a bad coding practice. Try to get rid of it.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.documentWriteCalls">
|
||||
<div class="label">document.write calls</div>
|
||||
<div class="result">{{phantomasResults.metrics.documentWriteCalls}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="documentWriteCallsTooltip = true"></div>
|
||||
<modal-dialog show="documentWriteCallsTooltip" dialog-title="document.write should have never existed">
|
||||
<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p>
|
||||
<p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href="https://github.com/krux/postscribe" target="_blank">PostScribe</a>.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.consoleMessages">
|
||||
<div class="label">Console messages</div>
|
||||
<div class="result">{{phantomasResults.metrics.consoleMessages}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="consoleMessagesTooltip = true"></div>
|
||||
<modal-dialog show="consoleMessagesTooltip" dialog-title="Avoid using console.log()">
|
||||
<p>Try to keep your console clean when in production. Debugging is good for development only.</p>
|
||||
<p>Writing in the console has a cost, especially when dumping large object variables.</p>
|
||||
<p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.globalVariables">
|
||||
<div class="label">Global variables</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.globalVariables}}
|
||||
<show-offenders modal-title="Global variables" metric-name="globalVariables" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="globalVariablesTooltip = true"></div>
|
||||
<modal-dialog show="globalVariablesTooltip" dialog-title="Minimize the use of global variables">
|
||||
<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p>
|
||||
<p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="inBodyDomManipulations > 10" ng-class="{'warning': inBodyDomManipulations > 200}">
|
||||
<div class="label">DOM manipulations in body</div>
|
||||
<div class="result">
|
||||
{{inBodyDomManipulations}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="inBodyDomManipulationsTooltip = true"></div>
|
||||
<modal-dialog show="inBodyDomManipulationsTooltip" dialog-title="Javascript executed inside the body">
|
||||
<p>This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.</p>
|
||||
<p>Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.</p>
|
||||
<p>The JS Timeline tab can help you identify what's happening.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jQueryLoading">{{notations.jQueryLoading}}</div>
|
||||
<div class="notation">jQuery version</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions == 1">
|
||||
<div class="label">jQuery version</div>
|
||||
<div class="result">{{phantomasResults.metrics.jQueryVersion}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jQueryVersionTooltip = true"></div>
|
||||
<modal-dialog show="jQueryVersionTooltip" dialog-title="Use the latest version of jQuery">
|
||||
<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p>
|
||||
<p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions > 1" class="warning">
|
||||
<div class="label">{{phantomasResults.metrics.jQueryDifferentVersions}} versions loaded</div>
|
||||
<div class="result">
|
||||
<span ng-repeat="version in phantomasResults.offenders.jQueryDifferentVersions">{{version}}<span ng-show="!$last"> & </span></span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jQueryDifferentVersionsTooltip = true"></div>
|
||||
<modal-dialog show="jQueryDifferentVersionsTooltip" dialog-title="Several versions of jQuery loaded">
|
||||
<p>jQuery is a heavy library. You should **never** load jQuery more than one on the same page.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.cssComplexity">{{notations.cssComplexity}}</div>
|
||||
<div class="notation">CSS complexity</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.cssRules" ng-class="{'warning': phantomasResults.metrics.cssRules > 4000 || (phantomasResults.metrics.cssRules > 1000 && phantomasResults.metrics.cssRules > phantomasResults.metrics.DOMelementsCount)}">
|
||||
<div class="label">Rules count</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRules}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssRulesTooltip" dialog-title="Clean up the CSS files">
|
||||
<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p>
|
||||
<p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom files per page.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssComplexSelectors" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectors > 2000}">
|
||||
<div class="label">Complex selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssComplexSelectors}}
|
||||
<show-offenders modal-title="Complex selectors" metric-name="cssComplexSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssComplexSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssComplexSelectorsTooltip" dialog-title="Avoid complex selectors">
|
||||
<p>Complex selectors are CSS selectors with 4 or more expressions, like "#header ul li .foo".</p>
|
||||
<p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssComplexSelectorsByAttribute" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectorsByAttribute > 100}">
|
||||
<div class="label">Complex attributes selector</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssComplexSelectorsByAttribute}}
|
||||
<show-offenders modal-title="Complex attributes selector" metric-name="cssComplexSelectorsByAttribute" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssComplexSelectorsByAttributeTooltip = true"></div>
|
||||
<modal-dialog show="cssComplexSelectorsByAttributeTooltip" dialog-title="Avoid complex attribute expressions">
|
||||
<p>Complex attributes selectors are one of these:
|
||||
<ul>
|
||||
<li>.foo[type*=bar] (contains bar)</li>
|
||||
<li>.foo[type^=bar] (starts with bar)</li>
|
||||
<li>.foo[type|=bar] (starts with bar or bar-)</li>
|
||||
<li>.foo[type$=bar] (ends with bar)</li>
|
||||
<li>.foo[type~=bar baz] (bar or baz)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>Their matching process needs more CPU and it has a cost on performances.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.badCss">{{notations.badCss}}</div>
|
||||
<div class="notation">Bad CSS</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.cssParsingErrors" class="warning">
|
||||
<div class="label">CSS syntax error</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssParsingErrors}}
|
||||
<show-offenders modal-title="CSS syntax errors" metric-name="cssParsingErrors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssRulesTooltip" dialog-title="Parsing error while analyzing CSS">
|
||||
<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p>
|
||||
<p>Maybe a <a href="http://jigsaw.w3.org/css-validator" target="_blank">CSS validator</a> can help you.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssImports" class="warning">
|
||||
<div class="label">Uses of @import</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssImports}}
|
||||
<show-offenders modal-title="Uses of @import" metric-name="cssImports" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssImportsTooltip = true"></div>
|
||||
<modal-dialog show="cssImportsTooltip" dialog-title="Don't use @import">
|
||||
<p>It’s bad for performance to use @import because CSS files don't get downloaded in parallel.</p>
|
||||
<p>You should use <link rel='stylesheet' href='a.css'> instead.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssDuplicatedSelectors" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedSelectors > 80}">
|
||||
<div class="label">Duplicated selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssDuplicatedSelectors}}
|
||||
<show-offenders modal-title="Duplicated selectors" metric-name="cssDuplicatedSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssDuplicatedSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssDuplicatedSelectorsTooltip" dialog-title="Merge duplicated selectors">
|
||||
<p>This is when two or more selectors are strictly identical and should be merged.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssDuplicatedProperties" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedProperties > 100}">
|
||||
<div class="label">Duplicated properties</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssDuplicatedProperties}}
|
||||
<show-offenders modal-title="Duplicated properties" metric-name="cssDuplicatedProperties" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssDuplicatedPropertiesTooltip = true"></div>
|
||||
<modal-dialog show="cssDuplicatedPropertiesTooltip" dialog-title="Remove duplicated properties">
|
||||
<p>This is the number of property definitions duplicated within a selector.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssEmptyRules" ng-class="{'warning': phantomasResults.metrics.cssEmptyRules > 100}">
|
||||
<div class="label">Empty rules</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssEmptyRules}}
|
||||
<show-offenders modal-title="Empty rules" metric-name="cssEmptyRules" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssEmptyRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssEmptyRulesTooltip" dialog-title="Eliminate empty rules">
|
||||
<p>Very easy to fix.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssExpressions">
|
||||
<div class="label">CSS expressions</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssExpressions}}
|
||||
<show-offenders modal-title="CSS expressions" metric-name="cssExpressions" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssExpressionsTooltip = true"></div>
|
||||
<modal-dialog show="cssExpressionsTooltip" dialog-title="Number of rules with CSS expressions">
|
||||
<p>Such as: expression( document.body.clientWidth > 600 ? "600px" : "auto" )</p>
|
||||
<p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssImportants">
|
||||
<div class="label">Uses of !important</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssImportants}}
|
||||
<show-offenders modal-title="Uses of !important" metric-name="cssImportants" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssImportantsTooltip = true"></div>
|
||||
<modal-dialog show="cssImportantsTooltip" dialog-title="Don't over-use !important">
|
||||
<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssOldIEFixes">
|
||||
<div class="label">Old IE fixes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssOldIEFixes}}
|
||||
<show-offenders modal-title="Old IE fixes" metric-name="cssOldIEFixes" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssOldIEFixesTooltip = true"></div>
|
||||
<modal-dialog show="cssOldIEFixesTooltip" dialog-title="Clean up old IE fixes">
|
||||
<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p>
|
||||
<p>IE6:
|
||||
<ul>
|
||||
<li>* html</li>
|
||||
<li>html > body (everything but IE6)</li>
|
||||
</ul>
|
||||
<p>
|
||||
<p>IE7:
|
||||
<ul>
|
||||
<li><b>*</b>height: 123px;</li>
|
||||
<li>height: 123px <b>!ie</b>;</li>
|
||||
</ul>
|
||||
<p>
|
||||
<p>IE9:
|
||||
<ul>
|
||||
<li>-ms-filter</li>
|
||||
<li>progid:DXImageTransform.Microsoft</li>
|
||||
</ul>
|
||||
</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssOldPropertyPrefixes">
|
||||
<div class="label">Old prefixes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssOldPropertyPrefixes}}
|
||||
<show-offenders modal-title="Old prefixes" metric-name="cssOldPropertyPrefixes" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssOldPropertyPrefixesTooltip = true"></div>
|
||||
<modal-dialog show="cssOldPropertyPrefixesTooltip" dialog-title="Clean up old CSS3 prefixes">
|
||||
<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssUniversalSelectors">
|
||||
<div class="label">Universal selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssUniversalSelectors}}
|
||||
<show-offenders modal-title="Universal selectors" metric-name="cssUniversalSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssUniversalSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssUniversalSelectorsTooltip" dialog-title="Avoid universal selectors">
|
||||
<p>Universal selectors are the most expensive CSS selectors.</p>
|
||||
<p>More informations <a href="http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/" target="_blank">here</a>.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssRedundantBodySelectors">
|
||||
<div class="label">Redundant body selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRedundantBodySelectors}}
|
||||
<show-offenders modal-title="Redundant body selectors" metric-name="cssRedundantBodySelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRedundantBodySelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssRedundantBodySelectorsTooltip" dialog-title="Useless redundant selectors">
|
||||
<p>This is one way to remove complexity from a CSS rule. Generally, when "body" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssRedundantChildNodesSelectors">
|
||||
<div class="label">Redundant tags selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRedundantChildNodesSelectors}}
|
||||
<show-offenders modal-title="Redundant tags selectors" metric-name="cssRedundantChildNodesSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRedundantChildNodesSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssRedundantChildNodesSelectorsTooltip" dialog-title="Useless redundant selectors">
|
||||
<p>Some tags included inside other tags are obvious. For example, when "ul li" is specified in a rule, "ul" can be removed because the "li" element is <b>always</b> inside a "ul". Same thing for "tr td", "select option", ...</p>
|
||||
<p>Lowering compexity in CSS selectors can make the page load a little faster.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.requests">{{notations.requests}}</div>
|
||||
<div class="notation">Requests number</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-class="{'warning': phantomasResults.metrics.requests > 200}">
|
||||
<div class="label">Total requests</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.requests}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="requestsTooltip = true"></div>
|
||||
<modal-dialog show="requestsTooltip" dialog-title="Minimize the number of requests">
|
||||
<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p>
|
||||
<p>There are several technics to reduce their number:
|
||||
<ul>
|
||||
<li>Concatenate JS files</li>
|
||||
<li>Concatenate CSS files</li>
|
||||
<li>Embed or inline small JS or CSS files in the HTML</li>
|
||||
<li>Create sprites or icon fonts</li>
|
||||
<li>Base64 encode small images in HTML or stylesheets</li>
|
||||
<li>Use lazyloading for images</li>
|
||||
</ul>
|
||||
</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.htmlCount">
|
||||
<div class="label">Documents</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.htmlCount}}
|
||||
<show-offenders modal-title="HTML count" metric-name="htmlCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jsCount" ng-class="{'warning': phantomasResults.metrics.jsCount > 40}">
|
||||
<div class="label">Scripts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsCount}}
|
||||
<show-offenders modal-title="JS count" metric-name="jsCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssCount" ng-class="{'warning': phantomasResults.metrics.cssCount > 25}">
|
||||
<div class="label">Stylesheets</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssCount}}
|
||||
<show-offenders modal-title="CSS count" metric-name="cssCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.imageCount" ng-class="{'warning': phantomasResults.metrics.imageCount > 80}">
|
||||
<div class="label">Images</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.imageCount}}
|
||||
<show-offenders modal-title="Image count" metric-name="imageCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.webfontCount" ng-class="{'warning': phantomasResults.metrics.webfontCount > 4}">
|
||||
<div class="label">Fonts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.webfontCount}}
|
||||
<show-offenders modal-title="Webfont count" metric-name="webfontCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.videoCount">
|
||||
<div class="label">Videos</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.videoCount}}
|
||||
<show-offenders modal-title="Video count" metric-name="videoCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jsonCount">
|
||||
<div class="label">JSON</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsonCount}}
|
||||
<show-offenders modal-title="JSON count" metric-name="jsonCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.otherCount">
|
||||
<div class="label">Other</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.otherCount}}
|
||||
<show-offenders modal-title="Other count" metric-name="otherCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.network">{{notations.network}}</div>
|
||||
<div class="notation">Network</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.notFound" class="warning">
|
||||
<div class="label">404 not found</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.notFound}}
|
||||
<show-offenders modal-title="404 not found" metric-name="notFound" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="notFoundTooltip = true"></div>
|
||||
<modal-dialog show="notFoundTooltip" dialog-title="404 errors">
|
||||
<p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.closedConnections" ng-class="{'warning': phantomasResults.metrics.closedConnections > 20}">
|
||||
<div class="label">Connections closed</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.closedConnections}}
|
||||
<show-offenders modal-title="Connections closed" metric-name="closedConnections" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="closedConnectionsTooltip = true"></div>
|
||||
<modal-dialog show="closedConnectionsTooltip" dialog-title="Connection keep-alive is important">
|
||||
<p>This counts the number of requests not keeping the connection alive (specifying "Connection: close" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p>
|
||||
<p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p>
|
||||
<p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.multipleRequests" ng-class="{'warning': phantomasResults.metrics.multipleRequests > 10}">
|
||||
<div class="label">Duplicated requests</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.multipleRequests}}
|
||||
<show-offenders modal-title="Static assets requested more than once" metric-name="multipleRequests" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="multipleRequestsTooltip = true"></div>
|
||||
<modal-dialog show="multipleRequestsTooltip" dialog-title="The same file is requested twice">
|
||||
<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingDisabled" ng-class="{'warning': phantomasResults.metrics.cachingDisabled > 25}">
|
||||
<div class="label">Caching disabled</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingDisabled}}
|
||||
<show-offenders modal-title="Caching disabled" metric-name="cachingDisabled" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingDisabledTooltip = true"></div>
|
||||
<modal-dialog show="cachingDisabledTooltip" dialog-title="Caching disabled">
|
||||
<p>Counts responses with caching disabled (max-age=0)</p>
|
||||
<p>Fix immediatly if on static assets.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingNotSpecified" ng-class="{'warning': phantomasResults.metrics.cachingNotSpecified > 50}">
|
||||
<div class="label">Caching not specified</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingNotSpecified}}
|
||||
<show-offenders modal-title="Caching not specified" metric-name="cachingNotSpecified" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingNotSpecifiedTooltip = true"></div>
|
||||
<modal-dialog show="cachingNotSpecifiedTooltip" dialog-title="No caching header">
|
||||
<p>Responses with no caching header sent (either Cache-Control or Expires).</p>
|
||||
<p>Every request should have a cache time specified. If you really don't want cache, specify "max-age=0", otherwise some browsers will try to cache.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingTooShort">
|
||||
<div class="label">Caching too short</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingTooShort}}
|
||||
<show-offenders modal-title="Caching too short" metric-name="cachingTooShort" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingTooShortTooltip = true"></div>
|
||||
<modal-dialog show="cachingTooShortTooltip" dialog-title="Increase cache time">
|
||||
<p>Responses with too short caching time (less than a week).</p>
|
||||
<p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.domains" ng-class="{'warning': phantomasResults.metrics.domains > 40}">
|
||||
<div class="label">Different domains</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.domains}}
|
||||
<show-offenders modal-title="Different domains" metric-name="domains" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="domainsTooltip = true"></div>
|
||||
<modal-dialog show="domainsTooltip" dialog-title="Reduce the number of domains">
|
||||
<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p>
|
||||
<p>By the way, domain sharding is not a good practice anymore.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="view == 'execution' && !phantomasResults.error" class="execution">
|
||||
<h2>Javascript Timeline</h2>
|
||||
<p>
|
||||
This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.
|
||||
</p>
|
||||
<div class="timeline">
|
||||
<div class="chart">
|
||||
<div class="chartPoints">
|
||||
<div ng-repeat="duration in timeline track by $index"
|
||||
class="interval"
|
||||
ng-class="{
|
||||
domCreation: $index * timelineIntervalDuration < phantomasResults.metrics.domInteractive,
|
||||
domInteractive: $index * timelineIntervalDuration >= phantomasResults.metrics.domInteractive
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoaded,
|
||||
domContentLoaded: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoaded
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoadedEnd,
|
||||
domContentLoadedEnd: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoadedEnd
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= phantomasResults.metrics.domComplete
|
||||
}">
|
||||
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
|
||||
<div class="tooltip detailsOverlay">
|
||||
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime">0 ms</div>
|
||||
<div class="endTime">{{endTime | number: 0}} ms</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="titles">
|
||||
<div class="domCreation"><div class="color"></div>DOM creation</div>
|
||||
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
|
||||
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
|
||||
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
|
||||
<div class="domComplete"><div class="color"></div>Page is complete</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
|
||||
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
|
||||
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
|
||||
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
|
||||
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Javascript Profiler</h2>
|
||||
<p>
|
||||
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
|
||||
</p>
|
||||
<div class="filters">
|
||||
<div>
|
||||
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Show warnings only</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" ng-model="textFilterOn" />
|
||||
Filter by
|
||||
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
|
||||
<toto data-info="mavariable"></toto>
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
<div>Timestamp</div>
|
||||
</div>
|
||||
<div ng-repeat="node in javascript.children"
|
||||
ng-if="(!warningsFilterOn
|
||||
|| (node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5)
|
||||
|| node.data.resultsNumber === 0
|
||||
|| node.data.callDetails.context.length === 0
|
||||
|| node.data.type == 'error'
|
||||
|| node.data.type == 'jQuery version change')
|
||||
&& (!textFilterOn
|
||||
|| !textFilter.length
|
||||
|| node.data.type.indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
|
||||
ng-class="{
|
||||
showingDetails: node.data.showDetails,
|
||||
jsError: node.data.type == 'error' || node.data.type == 'jQuery version change',
|
||||
windowPerformance: node.data.type == 'domInteractive' || node.data.type == 'domContentLoaded' || node.data.type == 'domContentLoadedEnd' || node.data.type == 'domComplete'
|
||||
}">
|
||||
<div class="index">{{$index + 1}}</div>
|
||||
<div class="type">{{node.data.type}}</div>
|
||||
|
||||
<div class="value">
|
||||
{{node.data.callDetails.arguments[0]}}
|
||||
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div ng-class="{
|
||||
'icon-question': !(node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5) && node.data.resultsNumber !== 0 && node.data.callDetails.context.length !== 0,
|
||||
'icon-warning': node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5 || node.data.resultsNumber === 0 || node.data.callDetails.context.length === 0
|
||||
}"
|
||||
ng-click="onNodeDetailsClick(node)"
|
||||
ng-if="node.data.type != 'jQuery loaded'
|
||||
&& node.data.type != 'jQuery version change'
|
||||
&& node.data.type != 'domInteractive'
|
||||
&& node.data.type != 'domContentLoaded'
|
||||
&& node.data.type != 'domContentLoadedEnd'
|
||||
&& node.data.type != 'domComplete'"></div>
|
||||
<div class="detailsOverlay" ng-show="node.data.showDetails">
|
||||
<div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.domElement">
|
||||
<h4>Called on DOM element</h4>
|
||||
<div>{{node.data.callDetails.context.domElement}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length === 0">
|
||||
<h4>Called on 0 jQuery element</h4>
|
||||
<p class="advice">Useless function call, as the jQuery object is empty.</p>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
|
||||
<h4>Called on 1 jQuery element</h4>
|
||||
<div>{{node.data.callDetails.context.firstElementPath}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length > 1">
|
||||
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
|
||||
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
|
||||
The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
|
||||
</p>
|
||||
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
|
||||
</div>
|
||||
|
||||
<p class="advice" ng-if="node.data.resultsNumber === 0">
|
||||
The query returned 0 results. Could it be unused or dead code?
|
||||
</p>
|
||||
|
||||
<div ng-if="node.data.parsedBacktrace">
|
||||
<h4>Backtrace</h4>
|
||||
<div class="table">
|
||||
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
|
||||
<div>{{trace.fnName || '(anonymous)'}}</div>
|
||||
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
|
||||
</div>
|
||||
<div ng-if="node.data.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
|
||||
<div>can't find any backtrace :/</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.children.length > 0">
|
||||
<h4>Sub processes</h4>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div>Duration</div>
|
||||
</div>
|
||||
<div ng-repeat="node in node.children">
|
||||
<div class="index">{{$index}}</div>
|
||||
<div class="type">{{node.data.type}}</div>
|
||||
<div class="value">
|
||||
{{node.data.callDetails.arguments[0]}}
|
||||
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
|
||||
</div>
|
||||
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms</div>
|
||||
<div class="duration" ng-if="node.data.time == undefined"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
var _phantomas_results = %%RESULTS%%;
|
||||
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,96.00C 144.341,96.00, 47.559,161.021,0.00,256.00c 47.559,94.979, 144.341,160.00, 256.00,160.00c 111.657,0.00, 208.439-65.021, 256.00-160.00 C 464.442,161.021, 367.657,96.00, 256.00,96.00z M 382.225,180.852c 30.081,19.187, 55.571,44.887, 74.717,75.148 c-19.146,30.261-44.637,55.961-74.718,75.149C 344.427,355.257, 300.779,368.00, 256.00,368.00c-44.78,0.00-88.428-12.743-126.225-36.852 C 99.695,311.962, 74.205,286.262, 55.058,256.00c 19.146-30.262, 44.637-55.962, 74.717-75.148c 1.959-1.25, 3.938-2.461, 5.929-3.65 C 130.725,190.866, 128.00,205.613, 128.00,221.00c0.00,70.692, 57.308,128.00, 128.00,128.00s 128.00-57.308, 128.00-128.00c0.00-15.387-2.725-30.134-7.704-43.799 C 378.286,178.39, 380.265,179.602, 382.225,180.852z M 256.00,205.00c0.00,26.51-21.49,48.00-48.00,48.00s-48.00-21.49-48.00-48.00s 21.49-48.00, 48.00-48.00 S 256.00,178.49, 256.00,205.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 224.00,384.00A32.00,32.00 2520.00 1,0 288.00,384A32.00,32.00 2520.00 1,0 224.00,384zM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 794 B |
@@ -1,5 +0,0 @@
|
||||
angular.module('YellowLabTools', [
|
||||
'Results',
|
||||
'ngModal',
|
||||
'ShowOffendersDirective'
|
||||
]);
|
||||
@@ -1,406 +0,0 @@
|
||||
var app = angular.module('Results', []);
|
||||
|
||||
app.controller('ResultsCtrl', function ($scope) {
|
||||
// Grab results from nodeJS served page
|
||||
$scope.phantomasResults = window._phantomas_results;
|
||||
|
||||
$scope.view = 'execution';
|
||||
|
||||
if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
|
||||
|
||||
// Get the execution tree from the offenders
|
||||
$scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
|
||||
|
||||
// Sort globalVariables offenders alphabetically
|
||||
if ($scope.phantomasResults.offenders.globalVariables) {
|
||||
$scope.phantomasResults.offenders.globalVariables.sort();
|
||||
}
|
||||
|
||||
|
||||
initSummaryView();
|
||||
initJSTimelineView();
|
||||
|
||||
}
|
||||
|
||||
$scope.setView = function(viewName) {
|
||||
$scope.view = viewName;
|
||||
};
|
||||
|
||||
$scope.onNodeDetailsClick = function(node) {
|
||||
var isOpen = node.data.showDetails;
|
||||
if (!isOpen) {
|
||||
// Close all other nodes
|
||||
$scope.javascript.children.forEach(function(currentNode) {
|
||||
currentNode.data.showDetails = false;
|
||||
});
|
||||
|
||||
// Parse the backtrace
|
||||
if (!node.data.parsedBacktrace) {
|
||||
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
|
||||
}
|
||||
|
||||
}
|
||||
node.data.showDetails = !isOpen;
|
||||
};
|
||||
|
||||
function initSummaryView() {
|
||||
|
||||
// Read the main elements of the tree and sum the total time
|
||||
$scope.totalJSTime = 0;
|
||||
$scope.inBodyDomManipulations = 0;
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
if (node.data.time) {
|
||||
$scope.totalJSTime += node.data.time;
|
||||
}
|
||||
|
||||
if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
|
||||
node.data.type !== 'jQuery - onDOMReady') {
|
||||
$scope.inBodyDomManipulations ++;
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Grab the notes
|
||||
$scope.notations = {
|
||||
domComplexity: getDomComplexityScore(),
|
||||
jsDomManipulations: getJsDomManipulationsScore(),
|
||||
jsBadPractices: getJSBadPracticesScore(),
|
||||
jQueryLoading: getJQueryLoadingScore(),
|
||||
cssComplexity: getCSSComplexityScore(),
|
||||
badCss: getBadCssScore(),
|
||||
requests: requestsScore(),
|
||||
network: networkScore()
|
||||
};
|
||||
}
|
||||
|
||||
function initJSTimelineView() {
|
||||
|
||||
if (!$scope.javascript.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
$scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
$scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
$scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
$scope.phantomasResults.metrics.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Now read the tree and display it on a timeline
|
||||
|
||||
// Split the timeline into 200 intervals
|
||||
var numberOfIntervals = 199;
|
||||
var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
|
||||
|
||||
// Pre-fill array of as many elements as there are milleseconds
|
||||
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the milliseconds array from the execution tree
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
|
||||
if (node.data.time !== undefined) {
|
||||
|
||||
// Ignore artefacts (durations > 100ms)
|
||||
var time = Math.min(node.data.time, 100) || 1;
|
||||
|
||||
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
|
||||
millisecondsArray[i] |= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill array of 200 elements
|
||||
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the timeline from the milliseconds array
|
||||
millisecondsArray.forEach(function(value, timestamp) {
|
||||
if (value === 1) {
|
||||
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Get the maximum value of the array (needed for display)
|
||||
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
|
||||
}
|
||||
|
||||
|
||||
function getDomComplexityScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.DOMelementsCount +
|
||||
Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
|
||||
$scope.phantomasResults.metrics.iframesCount * 50 +
|
||||
$scope.phantomasResults.metrics.DOMidDuplicated * 25;
|
||||
if (score > 1000) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 1500) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 2000) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 3000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 4000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJsDomManipulationsScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
|
||||
$scope.phantomasResults.metrics.DOMqueries +
|
||||
$scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
|
||||
$scope.phantomasResults.metrics.eventsBound;
|
||||
if (score > 300) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 500) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 700) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 1000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 1400) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJSBadPracticesScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
|
||||
$scope.phantomasResults.metrics.evalCalls * 2 +
|
||||
$scope.phantomasResults.metrics.jsErrors * 10 +
|
||||
$scope.phantomasResults.metrics.consoleMessages / 2 +
|
||||
$scope.phantomasResults.metrics.globalVariables / 20 +
|
||||
Math.sqrt($scope.inBodyDomManipulations);
|
||||
if (score > 10) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 15) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 20) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 30) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 45) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJQueryLoadingScore() {
|
||||
var note = 'NA';
|
||||
if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
|
||||
note = 'F';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion) {
|
||||
if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
|
||||
note = 'A';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
|
||||
note = 'B';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
|
||||
note = 'C';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
|
||||
note = 'D';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
|
||||
note = 'E';
|
||||
}
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getCSSComplexityScore() {
|
||||
if (!$scope.phantomasResults.metrics.cssRules) {
|
||||
return 'NA';
|
||||
}
|
||||
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.cssRules +
|
||||
$scope.phantomasResults.metrics.cssComplexSelectors * 5 +
|
||||
$scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
|
||||
if (score > 800) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 1200) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 2500) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 4000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 6000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getBadCssScore() {
|
||||
if ($scope.phantomasResults.metrics.cssParsingErrors) {
|
||||
return 'F';
|
||||
} else if (!$scope.phantomasResults.metrics.cssRules) {
|
||||
return 'NA';
|
||||
}
|
||||
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
|
||||
$scope.phantomasResults.metrics.cssDuplicatedProperties +
|
||||
$scope.phantomasResults.metrics.cssEmptyRules +
|
||||
$scope.phantomasResults.metrics.cssExpressions * 10 +
|
||||
$scope.phantomasResults.metrics.cssImportants * 2 +
|
||||
$scope.phantomasResults.metrics.cssOldIEFixes * 10 +
|
||||
$scope.phantomasResults.metrics.cssOldPropertyPrefixes +
|
||||
$scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
|
||||
$scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
|
||||
$scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
|
||||
$scope.phantomasResults.metrics.cssImports * 50;
|
||||
if (score > 50) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 200) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 500) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 1000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function requestsScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.requests;
|
||||
if (score > 30) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 45) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 60) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 80) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function networkScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.notFound * 25 +
|
||||
$scope.phantomasResults.metrics.closedConnections * 10 +
|
||||
$scope.phantomasResults.metrics.multipleRequests * 10 +
|
||||
$scope.phantomasResults.metrics.cachingDisabled * 2 +
|
||||
$scope.phantomasResults.metrics.cachingNotSpecified +
|
||||
$scope.phantomasResults.metrics.cachingTooShort / 2 +
|
||||
$scope.phantomasResults.metrics.domains;
|
||||
if (score > 20) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 40) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 60) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 80) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function parseBacktrace(str) {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var splited = str.split(' / ');
|
||||
splited.forEach(function(trace) {
|
||||
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
|
||||
if (result && result[2].length > 0) {
|
||||
var filePath = result[2];
|
||||
var chunks = filePath.split('/');
|
||||
var fileName = chunks[chunks.length - 1];
|
||||
|
||||
out.push({
|
||||
fnName: result[1],
|
||||
fileName: fileName,
|
||||
filePath: filePath,
|
||||
line: result[3]
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Goes on every node of the tree and calls the function fn. If fn returns false on a node, its children won't be checked.
|
||||
function treeRunner(node, fn) {
|
||||
if (fn(node) !== false && node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
treeRunner(child, fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
var app = angular.module("ShowOffendersDirective", []);
|
||||
|
||||
app.directive('showOffenders', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
modalTitle: "@",
|
||||
metricName: "@",
|
||||
phantomasResults: "="
|
||||
},
|
||||
controller: function($scope, $element, $attrs, $location) {
|
||||
$scope.dialogShown = false;
|
||||
},
|
||||
template: ' <span ng-click="dialogShown = true" class="icon-eye" title="See offenders"></span><modal-dialog show="dialogShown" dialog-title="{{modalTitle}}: {{phantomasResults.metrics[metricName]}}" width="70%"><ul><li ng-repeat="offender in phantomasResults.offenders[metricName] track by $index">{{offender}}</li></ul></modal-dialog>'
|
||||
};
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
.launchBtn {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
input[type=submit],
|
||||
input.url {
|
||||
padding: 0 0.5em;
|
||||
margin: 0.5em;
|
||||
font-size: 1.2em;
|
||||
height: 2em;
|
||||
border: 0 solid;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
outline: none;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
input[type=submit].clicked {
|
||||
color: #ddd;
|
||||
position: relative;
|
||||
left: 0.1em;
|
||||
top: 0.2em;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
@-webkit-keyframes rotating {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.waiting .icon-lab {
|
||||
-webkit-animation: rotating 3s linear 0s infinite;
|
||||
-webkit-transform: translateZ(0);
|
||||
animation: rotating 3s linear 0s infinite;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
@eye-font-family: "fontsmith-icons";
|
||||
@eye-value: "\e004";
|
||||
@eye: '"fontsmith-icons"' '"\\e004"';
|
||||
@lab-font-family: "fontsmith-icons";
|
||||
@lab-value: "\e003";
|
||||
@lab: '"fontsmith-icons"' '"\\e003"';
|
||||
@bars-font-family: "fontsmith-icons";
|
||||
@bars-value: "\e005";
|
||||
@bars: '"fontsmith-icons"' '"\\e005"';
|
||||
@warning-font-family: "fontsmith-icons";
|
||||
@warning-value: "\e000";
|
||||
@warning: '"fontsmith-icons"' '"\\e000"';
|
||||
@arrow-left3-font-family: "fontsmith-icons";
|
||||
@arrow-left3-value: "\e006";
|
||||
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
|
||||
@question-font-family: "fontsmith-icons";
|
||||
@question-value: "\e001";
|
||||
@question: '"fontsmith-icons"' '"\\e001"';
|
||||
@list-font-family: "fontsmith-icons";
|
||||
@list-value: "\e002";
|
||||
@list: '"fontsmith-icons"' '"\\e002"';
|
||||
|
||||
.icon-font-family(@char) {
|
||||
font-family: ~`@{char}[0]`;
|
||||
}
|
||||
|
||||
.icon-font() {
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.icon-content(@char) {
|
||||
content: ~`@{char}[1]`;
|
||||
}
|
||||
|
||||
.icon(@char) {
|
||||
.icon-font-family(@char);
|
||||
.icon-font();
|
||||
|
||||
&:before {
|
||||
.icon-content(@char);
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "fontsmith-icons";
|
||||
src:url("/public/fonts/icons.woff") format("woff"),
|
||||
;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.launchBtn {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input[type=submit], input.url {
|
||||
padding: 0 0.5em;
|
||||
margin: 0.5em;
|
||||
font-size: 1.2em;
|
||||
height: 2em;
|
||||
border: 0 solid;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
outline: none;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
input[type=submit].clicked {
|
||||
color: #ddd;
|
||||
position: relative;
|
||||
left: 0.1em;
|
||||
top: 0.2em;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotating {
|
||||
from { -webkit-transform: rotate(0deg); }
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.waiting .icon-lab {
|
||||
-webkit-animation: rotating 3s linear 0s infinite;
|
||||
-webkit-transform: translateZ(0);
|
||||
animation: rotating 3s linear 0s infinite;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
@import "icons.less";
|
||||
|
||||
html {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
background: #9c4274;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
|
||||
font-family: 'Century Gothic', helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #4e1836;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.star {
|
||||
font-weight: bold;
|
||||
span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.icon-lab {
|
||||
.icon(@lab);
|
||||
}
|
||||
.icon-question {
|
||||
.icon(@question);
|
||||
}
|
||||
.icon-warning {
|
||||
.icon(@warning);
|
||||
}
|
||||
.icon-back {
|
||||
.icon(@arrow-left3);
|
||||
}
|
||||
.icon-summary {
|
||||
.icon(@list);
|
||||
}
|
||||
.icon-spaghetti {
|
||||
.icon(@bars);
|
||||
}
|
||||
.icon-eye {
|
||||
.icon(@eye);
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
|
||||
/* Timeline colors, related to Window Performances */
|
||||
@domCreationColor: #FF6600;
|
||||
@domCreationBg: #FFE0CC;
|
||||
@domContentLoadedColor: #A7E846;
|
||||
@domContentLoadedBg: #E0FFD1;
|
||||
@domContentLoadedEndColor: #7ECCCC;
|
||||
@domContentLoadedEndBg: #D8F0F0;
|
||||
@domCompleteColor: #C2A3FF;
|
||||
@domCompleteBg: #EDE3FF;
|
||||
@domInteractiveColor: #FFE433;
|
||||
@domInteractiveBg: #FFFCCC;
|
||||
|
||||
|
||||
.resultsMenu {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.resultsMenu .menuItem {
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
width: 8em;
|
||||
height: 7em;
|
||||
color: #fff;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.resultsMenu .back {
|
||||
color: #5e2846;
|
||||
border-color: #5e2846;
|
||||
}
|
||||
.resultsMenu .menuItem div {
|
||||
padding-top: 0.5em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
|
||||
color: #ffa319;
|
||||
border-color: #ffa319;
|
||||
}
|
||||
.resultsMenu .menuItem:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
.resultsMenu span {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.summary, .metrics, .execution {
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.notations {
|
||||
display: table;
|
||||
width: 90%;
|
||||
margin: 0 10%;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
.notations > div {
|
||||
display: table-row;
|
||||
}
|
||||
.notations > div > div {
|
||||
display: table-cell;
|
||||
height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notations .notation {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria {
|
||||
font-weight: normal;
|
||||
}
|
||||
.notations .A, .notations .B, .notations .C, .notations .D, .notations .E, .notations .F, .notations .NA {
|
||||
width: 2.5em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.notations .A {
|
||||
/* green */
|
||||
background: #00DB61;
|
||||
}
|
||||
.notations .B {
|
||||
/* green */
|
||||
background: #CAD63D;
|
||||
}
|
||||
.notations .C {
|
||||
/* yellow */
|
||||
background: #FFD119;
|
||||
}
|
||||
.notations .D {
|
||||
/* orange */
|
||||
background: #FFA319;
|
||||
}
|
||||
.notations .E {
|
||||
/* red */
|
||||
background: #FF6600;
|
||||
}
|
||||
.notations .F {
|
||||
/* red */
|
||||
background: #FF1919;
|
||||
}
|
||||
.notations .NA {
|
||||
/* Non applicable */
|
||||
background: #CCC;
|
||||
}
|
||||
.notations .icon-eye {
|
||||
color: #9c4274;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notations .criteria .table {
|
||||
width: 75%;
|
||||
}
|
||||
.notations .criteria .label {
|
||||
width: 70%;
|
||||
}
|
||||
.notations .criteria .result {
|
||||
width: 20%;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .warning .label, .notations .warning .result {
|
||||
color: #FF1919;
|
||||
}
|
||||
.notations .criteria .info {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.timeline {
|
||||
margin: 2em 0 5em;
|
||||
}
|
||||
.timeline .chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.timeline .startTime, .timeline .endTime {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.timeline .startTime {
|
||||
left: 0em;
|
||||
}
|
||||
.timeline .endTime {
|
||||
right: 0em;
|
||||
}
|
||||
.timeline .chartPoints {
|
||||
display: table;
|
||||
height: 100px;
|
||||
width: 99%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeline .interval {
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
height: 100px;
|
||||
width: 0.5%;
|
||||
}
|
||||
.timeline .interval .color {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
.timeline .interval:hover .color {
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: @domCompleteBg;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: @domCompleteColor;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: @domContentLoadedEndBg;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: @domContentLoadedEndColor;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: @domContentLoadedBg;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: @domContentLoadedColor;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: @domInteractiveBg;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: @domInteractiveColor;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: @domCreationBg;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: @domCreationColor;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: auto;
|
||||
padding: 0.5em 1em;
|
||||
top: -1.5em;
|
||||
right: 1em;
|
||||
}
|
||||
.timeline .interval:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.timeline .legend {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .legend > div {
|
||||
display: table-row;
|
||||
}
|
||||
.timeline .legend > div > div {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .titles {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timeline .titles > div {
|
||||
padding: 0 1em 0 2em;
|
||||
}
|
||||
.timeline .tips {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.timeline .tips > div {
|
||||
padding: 1em 1em 0 0;
|
||||
}
|
||||
.timeline .legend .color {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
|
||||
.metrics h4 {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.metrics .module {
|
||||
padding-left: 4em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.metrics .legend {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.metrics .offenders {
|
||||
padding-left: 0em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.metrics .offenders div {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metrics .offenders ul {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
border: 1px dotted #aaa;
|
||||
}
|
||||
|
||||
.slowRequestsLimit {
|
||||
width: 3em;
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
input.textFilter {
|
||||
box-shadow: none;
|
||||
font-size: 1em;
|
||||
padding: 0 0.2em;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: none;
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
|
||||
.table > div {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.table > div > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.table > div.jsError > .type, .table > div.jsError > .value {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table > div.showingDetails > div {
|
||||
background: #f1c40f;
|
||||
}
|
||||
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.table > div > .type {
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.table > div > .details {
|
||||
position: relative;
|
||||
}
|
||||
.table .details .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table .details .icon-warning {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detailsOverlay {
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
width: 45em;
|
||||
min-height: 1em;
|
||||
padding: 0 1em 1em;
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay .advice {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.detailsOverlay .trace {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.table > div > .duration, .table > div > .startTime {
|
||||
text-align: center;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: @domCompleteBg;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: @domContentLoadedEndBg;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: @domContentLoadedBg;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: @domInteractiveBg;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: @domCreationBg;
|
||||
}
|
||||
|
||||
.table .icon-warning {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
|
||||
/**** NgModal popin (have a look inside bower_components) ****/
|
||||
.ng-modal {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.ng-modal-overlay {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ng-modal-dialog {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border: 3px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
.ng-modal-dialog-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-wrap: break-word;
|
||||
max-height: 20em;
|
||||
font-weight: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
.ng-modal-close {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.ng-modal-close-x {
|
||||
font-weight: bold;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.ng-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: solid 1px #999;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "fontsmith-icons";
|
||||
src: url("/public/fonts/icons.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
html {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
background: #9c4274;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
body,
|
||||
input[type=submit],
|
||||
input[type=text],
|
||||
input[type=url],
|
||||
input[type=number],
|
||||
button {
|
||||
font-family: 'Century Gothic', helvetica, arial, sans-serif;
|
||||
}
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
h1 span {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #ffa319;
|
||||
}
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #4e1836;
|
||||
}
|
||||
.footer a {
|
||||
color: inherit;
|
||||
}
|
||||
.footer .star {
|
||||
font-weight: bold;
|
||||
}
|
||||
.footer .star span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
/* Icons */
|
||||
.icon-lab {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-lab:before {
|
||||
content: "\e003";
|
||||
}
|
||||
.icon-question {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-question:before {
|
||||
content: "\e001";
|
||||
}
|
||||
.icon-warning {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-warning:before {
|
||||
content: "\e000";
|
||||
}
|
||||
.icon-back {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-back:before {
|
||||
content: "\e006";
|
||||
}
|
||||
.icon-summary {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-summary:before {
|
||||
content: "\e002";
|
||||
}
|
||||
.icon-spaghetti {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-spaghetti:before {
|
||||
content: "\e005";
|
||||
}
|
||||
.icon-eye {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "\e004";
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
/* Timeline colors, related to Window Performances */
|
||||
.resultsMenu {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.resultsMenu .menuItem {
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
width: 8em;
|
||||
height: 7em;
|
||||
color: #fff;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.resultsMenu .back {
|
||||
color: #5e2846;
|
||||
border-color: #5e2846;
|
||||
}
|
||||
.resultsMenu .menuItem div {
|
||||
padding-top: 0.5em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.resultsMenu .active,
|
||||
.resultsMenu .menuItem.active:hover {
|
||||
color: #ffa319;
|
||||
border-color: #ffa319;
|
||||
}
|
||||
.resultsMenu .menuItem:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.resultsMenu span {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
h4 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.summary,
|
||||
.metrics,
|
||||
.execution {
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
.notations {
|
||||
display: table;
|
||||
width: 90%;
|
||||
margin: 0 10%;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
.notations > div {
|
||||
display: table-row;
|
||||
}
|
||||
.notations > div > div {
|
||||
display: table-cell;
|
||||
height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notations .notation {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria {
|
||||
font-weight: normal;
|
||||
}
|
||||
.notations .A,
|
||||
.notations .B,
|
||||
.notations .C,
|
||||
.notations .D,
|
||||
.notations .E,
|
||||
.notations .F,
|
||||
.notations .NA {
|
||||
width: 2.5em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.notations .A {
|
||||
/* green */
|
||||
background: #00DB61;
|
||||
}
|
||||
.notations .B {
|
||||
/* green */
|
||||
background: #CAD63D;
|
||||
}
|
||||
.notations .C {
|
||||
/* yellow */
|
||||
background: #FFD119;
|
||||
}
|
||||
.notations .D {
|
||||
/* orange */
|
||||
background: #FFA319;
|
||||
}
|
||||
.notations .E {
|
||||
/* red */
|
||||
background: #FF6600;
|
||||
}
|
||||
.notations .F {
|
||||
/* red */
|
||||
background: #FF1919;
|
||||
}
|
||||
.notations .NA {
|
||||
/* Non applicable */
|
||||
background: #CCC;
|
||||
}
|
||||
.notations .icon-eye {
|
||||
color: #9c4274;
|
||||
cursor: pointer;
|
||||
}
|
||||
.notations .criteria .table {
|
||||
width: 75%;
|
||||
}
|
||||
.notations .criteria .label {
|
||||
width: 70%;
|
||||
}
|
||||
.notations .criteria .result {
|
||||
width: 20%;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .warning .label,
|
||||
.notations .warning .result {
|
||||
color: #FF1919;
|
||||
}
|
||||
.notations .criteria .info {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline {
|
||||
margin: 2em 0 5em;
|
||||
}
|
||||
.timeline .chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.timeline .startTime,
|
||||
.timeline .endTime {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.timeline .startTime {
|
||||
left: 0em;
|
||||
}
|
||||
.timeline .endTime {
|
||||
right: 0em;
|
||||
}
|
||||
.timeline .chartPoints {
|
||||
display: table;
|
||||
height: 100px;
|
||||
width: 99%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeline .interval {
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
height: 100px;
|
||||
width: 0.5%;
|
||||
}
|
||||
.timeline .interval .color {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
.timeline .interval:hover .color {
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: #c2a3ff;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: #7ecccc;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: #a7e846;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: #fffccc;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: #ffe433;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: #ff6600;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: auto;
|
||||
padding: 0.5em 1em;
|
||||
top: -1.5em;
|
||||
right: 1em;
|
||||
}
|
||||
.timeline .interval:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
.timeline .legend {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .legend > div {
|
||||
display: table-row;
|
||||
}
|
||||
.timeline .legend > div > div {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .titles {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timeline .titles > div {
|
||||
padding: 0 1em 0 2em;
|
||||
}
|
||||
.timeline .tips {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.timeline .tips > div {
|
||||
padding: 1em 1em 0 0;
|
||||
}
|
||||
.timeline .legend .color {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
.metrics h4 {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.metrics .module {
|
||||
padding-left: 4em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.metrics .legend {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
}
|
||||
.metrics .offenders {
|
||||
padding-left: 0em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.metrics .offenders div {
|
||||
cursor: pointer;
|
||||
}
|
||||
.metrics .offenders ul {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.filters {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
border: 1px dotted #aaa;
|
||||
}
|
||||
.slowRequestsLimit {
|
||||
width: 3em;
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
input.textFilter {
|
||||
box-shadow: none;
|
||||
font-size: 1em;
|
||||
padding: 0 0.2em;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: none;
|
||||
width: 15em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
.table > div {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.table > div.jsError > .type,
|
||||
.table > div.jsError > .value {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.table > div.windowPerformance > div,
|
||||
.table > div.windowPerformance > div.startTime {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table > div.showingDetails > div {
|
||||
background: #f1c40f;
|
||||
}
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
}
|
||||
.table > div > .type {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .details {
|
||||
position: relative;
|
||||
}
|
||||
.table .details .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table .details .icon-warning {
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay {
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
width: 45em;
|
||||
min-height: 1em;
|
||||
padding: 0 1em 1em;
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay .advice {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.detailsOverlay .trace {
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .duration,
|
||||
.table > div > .startTime {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: #fffccc;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.table .icon-warning {
|
||||
color: #e74c3c;
|
||||
}
|
||||
/**** NgModal popin (have a look inside bower_components) ****/
|
||||
.ng-modal {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.ng-modal-overlay {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ng-modal-dialog {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border: 3px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
.ng-modal-dialog-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-wrap: break-word;
|
||||
max-height: 20em;
|
||||
font-weight: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
.ng-modal-close {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.ng-modal-close-x {
|
||||
font-weight: bold;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.ng-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: solid 1px #999;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
var EasyXml = require('easyxml');
|
||||
|
||||
var ylt = require('../lib/index');
|
||||
|
||||
var cli = meow({
|
||||
help: [
|
||||
'Usage',
|
||||
' yellowlabtools <url> <options>',
|
||||
'',
|
||||
'Options:',
|
||||
' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
|
||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
||||
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
|
||||
' --cookie Adds a cookie on the main domain.',
|
||||
' --auth-user Basic HTTP authentication username.',
|
||||
' --auth-pass Basic HTTP authentication password.',
|
||||
' --block-domain Disallow requests to given (comma-separated) domains.',
|
||||
' --allow-domain Only allow requests to given (comma-separated) domains.',
|
||||
' --no-externals Block all domains except the main one.',
|
||||
' --reporter The output format: "json" or "xml". Default is "json".',
|
||||
' --local-storage Ability to set a local storage, key-value pairs (e.g. "bar=foo;domain=url")',
|
||||
' --session-storage Ability to set a session storage, key-value pairs (e.g. "bar=foo;domain=url")',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: require('../package.json')
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Check parameters
|
||||
if (cli.input.length < 1) {
|
||||
cli.showHelp();
|
||||
}
|
||||
|
||||
var url = cli.input[0];
|
||||
|
||||
var options = {};
|
||||
|
||||
// Screenshot option
|
||||
var screenshot = cli.flags.screenshot;
|
||||
if (screenshot && (typeof screenshot !== 'string' || screenshot.toLowerCase().indexOf('.png', screenshot.length - 4) === -1)) {
|
||||
console.error('Incorrect parameters: screenshot must be a path that ends with ".png"');
|
||||
process.exit(1);
|
||||
}
|
||||
if (screenshot) {
|
||||
if (path.resolve(screenshot) !== path.normalize(screenshot)) {
|
||||
// It is not an absolute path, so it is relative to the current command-line directory
|
||||
screenshot = path.join(process.cwd(), screenshot);
|
||||
}
|
||||
options.screenshot = cli.flags.screenshot;
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'mobile';
|
||||
|
||||
// Wait for CSS selector
|
||||
options.waitForSelector = cli.flags.waitForSelector || null;
|
||||
|
||||
// Proxy
|
||||
options.proxy = cli.flags.proxy || null;
|
||||
|
||||
// Cookie
|
||||
options.cookie = cli.flags.cookie || null;
|
||||
|
||||
// HTTP basic auth
|
||||
options.authUser = cli.flags.authUser || null;
|
||||
options.authPass = cli.flags.authPass || null;
|
||||
|
||||
// Domain blocking
|
||||
options.blockDomain = cli.flags.blockDomain || null;
|
||||
options.allowDomain = cli.flags.allowDomain || null;
|
||||
options.noExternals = cli.flags.noExternals || null;
|
||||
|
||||
// Storage injection
|
||||
options.localStorage = cli.flags.localStorage;
|
||||
options.sessionStorage = cli.flags.sessionStorage;
|
||||
|
||||
// Output format
|
||||
if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') {
|
||||
console.error('Incorrect parameters: reporter has to be "json" or "xml"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
(function execute(url, options) {
|
||||
'use strict';
|
||||
|
||||
ylt(url, options).
|
||||
|
||||
then(function(data) {
|
||||
|
||||
debug('Success');
|
||||
switch(cli.flags.reporter) {
|
||||
case 'xml':
|
||||
var serializer = new EasyXml({
|
||||
manifest: true
|
||||
});
|
||||
|
||||
// Remove some heavy parts of the results object
|
||||
delete data.toolsResults;
|
||||
|
||||
var xmlOutput = serializer.render(data);
|
||||
|
||||
// Remove special chars from XML tags: # [ ]
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)#([^>]*)>/g, '<$1>');
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)\[([^>]*)>/g, '<$1>');
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)\]([^>]*)>/g, '<$1>');
|
||||
|
||||
// Remove special chars from text content: \n \0
|
||||
xmlOutput = xmlOutput.replace(/(<[a-zA-Z]*>[^<]*)\n([^<]*<\/[a-zA-Z]*>)/g, '$1$2');
|
||||
xmlOutput = xmlOutput.replace(/\0/g, '');
|
||||
xmlOutput = xmlOutput.replace(/\uFFFF/g, '');
|
||||
xmlOutput = xmlOutput.replace(/\u0002/g, '');
|
||||
|
||||
console.log(xmlOutput);
|
||||
break;
|
||||
default:
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
}).fail(function(err) {
|
||||
|
||||
debug('Test failed for %s', url);
|
||||
console.error(err);
|
||||
|
||||
});
|
||||
|
||||
debug('Test launched...');
|
||||
|
||||
})(url, options);
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"dependencies": {
|
||||
"angular": "~1.3.1",
|
||||
"ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,79 @@
|
||||
var debug = require('debug')('ylt:index');
|
||||
var Q = require('q');
|
||||
|
||||
var Runner = require('./runner');
|
||||
var ScreenshotHandler = require('./screenshotHandler');
|
||||
|
||||
var packageJson = require('../package.json');
|
||||
|
||||
|
||||
var yellowLabTools = function(url, options) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!url) {
|
||||
|
||||
deferred.reject('URL missing');
|
||||
|
||||
} else {
|
||||
|
||||
if (url.toLowerCase().indexOf('http://') !== 0 && url.toLowerCase().indexOf('https://') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
var params = {
|
||||
url: url,
|
||||
options: options || {}
|
||||
};
|
||||
|
||||
var runner = new Runner(params)
|
||||
|
||||
.progress(deferred.notify)
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// If a screenshot saveFunction was provided in the options
|
||||
if (options && typeof options.saveScreenshotFn === 'function') {
|
||||
debug('Now optimizing screenshot...');
|
||||
|
||||
// TODO: temporarily set all screenshot sizes to 600px, until we find a solution
|
||||
ScreenshotHandler.findAndOptimizeScreenshot(data.params.options.screenshot, 600)
|
||||
|
||||
.then(function(screenshotBuffer) {
|
||||
debug('Screenshot optimized, now saving...');
|
||||
|
||||
return options.saveScreenshotFn('screenshot.jpg', screenshotBuffer);
|
||||
})
|
||||
|
||||
.then(function(response) {
|
||||
debug('Screenshot saved');
|
||||
debug(response);
|
||||
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
})
|
||||
|
||||
.catch(function(err) {
|
||||
// It's ok if we can't save the screenshot
|
||||
debug('Screenshot could not be saved');
|
||||
debug(err);
|
||||
})
|
||||
|
||||
.finally(function() {
|
||||
deferred.resolve(data);
|
||||
});
|
||||
|
||||
} else {
|
||||
deferred.resolve(data);
|
||||
}
|
||||
})
|
||||
|
||||
.catch(function(err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
module.exports = yellowLabTools;
|
||||
module.exports.version = packageJson.version;
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"globalScore": {
|
||||
"pageWeight": 2,
|
||||
"images": 2,
|
||||
"domComplexity": 1,
|
||||
"javascriptComplexity": 2,
|
||||
"badJavascript": 2,
|
||||
"jQuery": 0.5,
|
||||
"cssComplexity": 0.5,
|
||||
"badCSS": 1,
|
||||
"fonts": 1,
|
||||
"serverConfig": 1
|
||||
},
|
||||
"categories": {
|
||||
"pageWeight": {
|
||||
"label": "Network",
|
||||
"policies": {
|
||||
"totalWeight": 5,
|
||||
"totalRequests": 2,
|
||||
"domains": 3,
|
||||
"compression": 2,
|
||||
"fileMinification": 2,
|
||||
"identicalFiles": 2,
|
||||
"emptyRequests": 3,
|
||||
"notFound": 2
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"label": "Images",
|
||||
"policies": {
|
||||
"imagesNotOptimized": 2,
|
||||
"imagesOldFormat": 1,
|
||||
"imagesScaledDown": 2,
|
||||
"imagesExcessiveDensity": 0.25,
|
||||
"imagesWithIncorrectSizesParam": 1,
|
||||
"lazyLoadableImagesBelowTheFold": 2,
|
||||
"hiddenImages": 1
|
||||
}
|
||||
},
|
||||
"domComplexity": {
|
||||
"label": "DOM complexity",
|
||||
"policies": {
|
||||
"DOMelementsCount": 3,
|
||||
"DOMelementMaxDepth": 1,
|
||||
"iframesCount": 1,
|
||||
"DOMidDuplicated": 1
|
||||
}
|
||||
},
|
||||
"javascriptComplexity": {
|
||||
"label": "JS complexity",
|
||||
"policies": {
|
||||
"scriptDuration": 4,
|
||||
"DOMaccesses": 2,
|
||||
"eventsScrollBound": 1
|
||||
}
|
||||
},
|
||||
"badJavascript": {
|
||||
"label": "Bad JS",
|
||||
"policies": {
|
||||
"jsErrors": 1,
|
||||
"documentWriteCalls": 2,
|
||||
"synchronousXHR": 5,
|
||||
"globalVariables": 0.5
|
||||
}
|
||||
},
|
||||
"jQuery": {
|
||||
"label": "jQuery",
|
||||
"policies": {
|
||||
"jQueryVersion": 2,
|
||||
"jQueryVersionsLoaded": 2
|
||||
}
|
||||
},
|
||||
"cssComplexity": {
|
||||
"label": "CSS complexity",
|
||||
"policies": {
|
||||
"cssRules": 2,
|
||||
"cssComplexSelectors": 2,
|
||||
"cssColors": 0.5,
|
||||
"similarColors": 0.5,
|
||||
"cssBreakpoints": 0.5,
|
||||
"cssMobileFirst": 1
|
||||
}
|
||||
},
|
||||
"badCSS": {
|
||||
"label": "Bad CSS",
|
||||
"policies": {
|
||||
"cssParsingErrors": 3,
|
||||
"cssImports": 4,
|
||||
"cssDuplicatedSelectors": 2,
|
||||
"cssDuplicatedProperties": 1,
|
||||
"cssEmptyRules": 2,
|
||||
"cssImportants": 2,
|
||||
"cssOldIEFixes": 1,
|
||||
"cssOldPropertyPrefixes": 1,
|
||||
"cssRedundantBodySelectors": 1,
|
||||
"cssRedundantChildNodesSelectors": 1
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"label": "Web fonts",
|
||||
"policies": {
|
||||
"fontsCount": 1,
|
||||
"heavyFonts": 0.5,
|
||||
"unusedUnicodeRanges": 0.5,
|
||||
"nonWoff2Fonts": 0.5
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"label": "Server config",
|
||||
"policies": {
|
||||
"oldHttpProtocol": 2,
|
||||
"oldTlsProtocol": 2,
|
||||
"closedConnections": 2,
|
||||
"cachingNotSpecified": 1,
|
||||
"cachingDisabled": 1,
|
||||
"cachingTooShort": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
|
||||
|
||||
var OffendersHelpers = function() {
|
||||
|
||||
this.domPathToArray = function(str) {
|
||||
return str.split(/\s?>\s?/);
|
||||
};
|
||||
|
||||
this.listOfDomArraysToTree = function(listOfDomArrays) {
|
||||
var result = {};
|
||||
|
||||
function recursiveTreeBuilder(tree, domArray) {
|
||||
if (domArray.length > 0) {
|
||||
var currentDomElement = domArray.shift();
|
||||
if (tree === null) {
|
||||
tree = {};
|
||||
}
|
||||
tree[currentDomElement] = recursiveTreeBuilder(tree[currentDomElement] || null, domArray);
|
||||
return tree;
|
||||
} else if (tree === null) {
|
||||
return 1;
|
||||
} else {
|
||||
return tree + 1;
|
||||
}
|
||||
}
|
||||
|
||||
listOfDomArrays.forEach(function(domArray) {
|
||||
result = recursiveTreeBuilder(result, domArray);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
this.domPathToDomElementObj = function(domPath) {
|
||||
|
||||
if (typeof domPath === 'boolean') {
|
||||
return {
|
||||
// Not a normal element path
|
||||
type: 'notAnElement',
|
||||
element: domPath
|
||||
};
|
||||
}
|
||||
|
||||
var domArray = this.domPathToArray(domPath);
|
||||
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
||||
|
||||
if (domArray[0] === 'html') {
|
||||
return {
|
||||
type: 'html'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'body') {
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'body'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'domElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
}
|
||||
if (domArray[0] === 'head') {
|
||||
return {
|
||||
type: 'head'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === '#document') {
|
||||
return {
|
||||
type: 'document'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'window') {
|
||||
return {
|
||||
type: 'window'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'DocumentFragment') {
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'fragment'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'fragmentElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Not attached element, such as just created with document.createElement()
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'createdElement',
|
||||
element: domPath
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'createdElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.backtraceToArray = function(str) {
|
||||
var traceArray = str.split(/ \/ /);
|
||||
|
||||
if (traceArray) {
|
||||
var results = [];
|
||||
var parts = null;
|
||||
var obj;
|
||||
|
||||
for (var i=0 ; i<traceArray.length ; i++) {
|
||||
// Handle the new PhantomJS 2.x syntax
|
||||
parts = /^\s*at( (\w+))? \(?([^ ]+):(\d+):(\d+)\)?$$/.exec(traceArray[i]);
|
||||
|
||||
if (parts) {
|
||||
obj = {
|
||||
file: parts[3],
|
||||
line: parseInt(parts[4], 10),
|
||||
column: parseInt(parts[5], 10)
|
||||
};
|
||||
|
||||
if (parts[2]) {
|
||||
obj.functionName = parts[2];
|
||||
}
|
||||
|
||||
results.push(obj);
|
||||
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.sortVarsLikeChromeDevTools = function(vars) {
|
||||
return vars.sort(function(a, b) {
|
||||
return (a < b) ? -1 : 1;
|
||||
});
|
||||
};
|
||||
|
||||
this.urlToLink = function(url) {
|
||||
var shortUrl = (url.length > 110) ? url.substr(0, 47) + ' ... ' + url.substr(-48) : url;
|
||||
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortUrl + '</a>';
|
||||
};
|
||||
|
||||
this.cssOffenderPattern = function(offender) {
|
||||
// Used to work with strings
|
||||
// As of Phantomas v2, offender is now in JSON format.
|
||||
// Let's just adapt this for now and we'll see later if we remove completely this function
|
||||
return {
|
||||
css: offender.value.message,
|
||||
file: offender.url,
|
||||
line: offender.value.position.start.line,
|
||||
column: offender.value.position.start.column
|
||||
};
|
||||
};
|
||||
|
||||
this.fileWithSizePattern = function(fileWithSize) {
|
||||
var parts = /^([^ ]*) \((\d+\.\d{2}|NaN) kB\)$/.exec(fileWithSize);
|
||||
|
||||
if (!parts) {
|
||||
return {
|
||||
file: fileWithSize
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
file: parts[1],
|
||||
size: parseFloat(parts[2])
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.orderByFile = function(offenders) {
|
||||
var byFileObj = {};
|
||||
|
||||
offenders.forEach(function(offender) {
|
||||
var file = offender.file || 'Inline CSS';
|
||||
delete offender.file;
|
||||
|
||||
if (!byFileObj[file]) {
|
||||
byFileObj[file] = {
|
||||
url: file,
|
||||
count: 0,
|
||||
offenders: []
|
||||
};
|
||||
}
|
||||
|
||||
byFileObj[file].count ++;
|
||||
byFileObj[file].offenders.push(offender);
|
||||
});
|
||||
|
||||
// Transform object into array
|
||||
var byFileArray = [];
|
||||
for (var file in byFileObj) {
|
||||
byFileArray.push(byFileObj[file]);
|
||||
}
|
||||
|
||||
return {byFile: byFileArray};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = new OffendersHelpers();
|
||||
@@ -0,0 +1,144 @@
|
||||
var extend = require('util')._extend;
|
||||
|
||||
var debug = require('debug')('ylt:ruleschecker');
|
||||
|
||||
var RulesChecker = function() {
|
||||
'use strict';
|
||||
|
||||
this.check = function(data, policies) {
|
||||
/*jshint loopfunc:true */
|
||||
|
||||
var results = {};
|
||||
|
||||
debug('Starting checking rules');
|
||||
|
||||
for (var metricName in policies) {
|
||||
var policy = policies[metricName];
|
||||
var rule;
|
||||
|
||||
if (policy.tool &&
|
||||
data.toolsResults[policy.tool] &&
|
||||
data.toolsResults[policy.tool].metrics &&
|
||||
(data.toolsResults[policy.tool].metrics[metricName] || data.toolsResults[policy.tool].metrics[metricName] === 0)) {
|
||||
|
||||
rule = {
|
||||
value: data.toolsResults[policy.tool].metrics[metricName],
|
||||
policy: extend({}, policy) // Clone object policy instead of reference
|
||||
};
|
||||
|
||||
|
||||
// Deal with offenders
|
||||
if (policy.hasOffenders) {
|
||||
|
||||
var offenders = [];
|
||||
|
||||
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
|
||||
if (policy.takeOffendersFrom) {
|
||||
|
||||
var fromList = policy.takeOffendersFrom;
|
||||
|
||||
// takeOffendersFrom option can be a string or an array of strings.
|
||||
if (typeof fromList === 'string') {
|
||||
fromList = [fromList];
|
||||
}
|
||||
|
||||
fromList.forEach(function(from) {
|
||||
if (data.toolsResults[policy.tool] &&
|
||||
data.toolsResults[policy.tool].offenders &&
|
||||
data.toolsResults[policy.tool].offenders[from]) {
|
||||
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
|
||||
}
|
||||
});
|
||||
|
||||
data.toolsResults[policy.tool].offenders[metricName] = offenders;
|
||||
|
||||
} else if (data.toolsResults[policy.tool] &&
|
||||
data.toolsResults[policy.tool].offenders &&
|
||||
data.toolsResults[policy.tool].offenders[metricName]) {
|
||||
offenders = data.toolsResults[policy.tool].offenders[metricName];
|
||||
}
|
||||
|
||||
// It is possible to declare a transformation function for the main metric value.
|
||||
// The function should
|
||||
if (policy.valueTransformFn) {
|
||||
rule.value = policy.valueTransformFn(offenders);
|
||||
}
|
||||
|
||||
|
||||
var offendersObj = {};
|
||||
|
||||
// It is possible to declare a transformation function for the offenders.
|
||||
// The function should take an array of strings as single parameter and return a string.
|
||||
if (policy.offendersTransformFn) {
|
||||
|
||||
try {
|
||||
offendersObj = policy.offendersTransformFn(offenders, rule);
|
||||
} catch(err) {
|
||||
debug('Error while transforming offenders for %s', metricName);
|
||||
debug(err);
|
||||
}
|
||||
|
||||
delete rule.policy.offendersTransformFn;
|
||||
|
||||
} else {
|
||||
|
||||
offendersObj = {
|
||||
count: offenders.length,
|
||||
list: offenders
|
||||
};
|
||||
}
|
||||
|
||||
rule.offendersObj = offendersObj;
|
||||
}
|
||||
|
||||
|
||||
rule.bad = rule.value > policy.isOkThreshold;
|
||||
rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
|
||||
|
||||
// A value between 0 (bad) and 100 (very good).
|
||||
var score = (policy.isBadThreshold - rule.value) * 100 / (policy.isBadThreshold - policy.isOkThreshold);
|
||||
rule.score = Math.min(Math.max(Math.round(score), 0), 100);
|
||||
|
||||
// A value between 0 (abnormal) and negative-infinity (your website is a blackhole)
|
||||
var abnormalityScore = (policy.isAbnormalThreshold - rule.value) * 100 / (policy.isAbnormalThreshold - policy.isOkThreshold);
|
||||
rule.abnormalityScore = Math.min(Math.round(abnormalityScore), 0);
|
||||
|
||||
results[metricName] = rule;
|
||||
debug('Metric %s calculated. Score: %d', metricName, rule.score);
|
||||
|
||||
|
||||
} else if (policy.scoreFn) {
|
||||
|
||||
debug('Custom score function for %s', metricName);
|
||||
|
||||
// Custom score function
|
||||
rule = policy.scoreFn(data);
|
||||
|
||||
// Check returned values (if the result is null, just don't save)
|
||||
if (rule) {
|
||||
rule.policy = {
|
||||
label: policy.label,
|
||||
message: policy.message,
|
||||
hasOffenders: policy.hasOffenders || false
|
||||
};
|
||||
|
||||
results[metricName] = rule;
|
||||
debug('Metric %s calculated. Score: %d', metricName, rule.score);
|
||||
} else {
|
||||
debug('Metric %s is null. Ignored.', metricName);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
debug('Metric %s not found for tool %s', metricName, policy.tool);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
debug('Rules checking finished');
|
||||
|
||||
return results;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new RulesChecker();
|
||||
@@ -0,0 +1,141 @@
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:runner');
|
||||
|
||||
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
||||
var colorDiff = require('./tools/colorDiff');
|
||||
var domAccessAgregator = require('./tools/domAccessAgregator');
|
||||
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
|
||||
var redownload = require('./tools/redownload/redownload');
|
||||
var rulesChecker = require('./rulesChecker');
|
||||
var scoreCalculator = require('./scoreCalculator');
|
||||
|
||||
|
||||
var Runner = function(params) {
|
||||
'use strict';
|
||||
|
||||
var deferred = Q.defer();
|
||||
|
||||
// The pivot format
|
||||
var data = {
|
||||
params: params,
|
||||
toolsResults: {}
|
||||
};
|
||||
|
||||
// Execute Phantomas first
|
||||
phantomasWrapper.execute(data)
|
||||
|
||||
// For the progress bar
|
||||
.progress(function(event) {
|
||||
if (event === 'domReady') {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.15,
|
||||
milestone: 'domReady'
|
||||
});
|
||||
}
|
||||
if (event === 'domComplete') {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.25,
|
||||
milestone: 'domComplete'
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.then(function(phantomasResults) {
|
||||
// For the progress bar
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.4,
|
||||
milestone: 'phantomas'
|
||||
});
|
||||
|
||||
data.toolsResults.phantomas = phantomasResults;
|
||||
|
||||
// Mix all DOM Access metrics together
|
||||
data = domAccessAgregator.agregate(data);
|
||||
|
||||
// Compare colors
|
||||
data = colorDiff.compareAllColors(data);
|
||||
|
||||
// Check media queries
|
||||
data = mediaQueriesChecker.analyzeMediaQueries(data);
|
||||
|
||||
// Redownload every file
|
||||
return redownload.recheckAllFiles(data)
|
||||
|
||||
.progress(function(redownloadedProgress) {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.4 + redownloadedProgress * 0.55,
|
||||
milestone: 'phantomas'
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// For the progress bar
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.99,
|
||||
milestone: 'redownload'
|
||||
});
|
||||
|
||||
// Fix: don't display Unicode ranges if the module is not present in Phantomas
|
||||
if (!data.toolsResults.phantomas.metrics.differentCharacters) {
|
||||
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
|
||||
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
|
||||
}
|
||||
|
||||
// Rules checker
|
||||
var policies = require('./metadata/policies');
|
||||
data.rules = rulesChecker.check(data, policies);
|
||||
|
||||
|
||||
// Scores calculator
|
||||
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
|
||||
data.scoreProfiles = {
|
||||
generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
|
||||
};
|
||||
|
||||
|
||||
// Calculate "If you fix this issue, your new score is..." on each rule
|
||||
debug('Calculate "If you fix this issue..." scores');
|
||||
Object.keys(data.rules).forEach(function(ruleName) {
|
||||
// Save current values
|
||||
var oldScore = data.rules[ruleName].score;
|
||||
var oldAbnormalityScore = data.rules[ruleName].abnormalityScore;
|
||||
// Simulate a 100/100 score on a specific rule
|
||||
data.rules[ruleName].score = 100;
|
||||
data.rules[ruleName].abnormalityScore = 0;
|
||||
// Calculate new score
|
||||
data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore;
|
||||
// Revert values
|
||||
data.rules[ruleName].score = oldScore;
|
||||
data.rules[ruleName].abnormalityScore = oldAbnormalityScore;
|
||||
});
|
||||
debug('Calculating is finished');
|
||||
|
||||
|
||||
if (data.toolsResults.phantomas.offenders.blockedRequests) {
|
||||
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
|
||||
}
|
||||
|
||||
// Report WordPress detection
|
||||
if (data.toolsResults.redownload.metrics.isWordPress === true) {
|
||||
data.frameworks = {isWordPress: true};
|
||||
}
|
||||
|
||||
// Finished!
|
||||
deferred.resolve(data);
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
debug('Run failed');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
});
|
||||
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
module.exports = Runner;
|
||||
@@ -0,0 +1,91 @@
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:scoreCalculator');
|
||||
|
||||
var ScoreCalculator = function() {
|
||||
'use strict';
|
||||
|
||||
this.calculate = function(data, profile, debugFlag) {
|
||||
|
||||
var results = {
|
||||
categories: {}
|
||||
};
|
||||
var categoryScore;
|
||||
var categoryName;
|
||||
var weight;
|
||||
|
||||
if (debugFlag) {
|
||||
debug('Starting calculating scores');
|
||||
}
|
||||
|
||||
// Calculate categories
|
||||
for (categoryName in profile.categories) {
|
||||
var categoryResult = {
|
||||
label: profile.categories[categoryName].label
|
||||
};
|
||||
|
||||
categoryScore = new ScoreMerger();
|
||||
var rules = [];
|
||||
var policyScore;
|
||||
|
||||
for (var policyName in profile.categories[categoryName].policies) {
|
||||
weight = profile.categories[categoryName].policies[policyName];
|
||||
|
||||
if (data.rules[policyName]) {
|
||||
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
|
||||
categoryScore.push(policyScore, weight);
|
||||
} else if (debugFlag) {
|
||||
debug('Warning: could not find rule %s', policyName);
|
||||
}
|
||||
|
||||
rules.push(policyName);
|
||||
}
|
||||
|
||||
categoryResult.categoryScore = categoryScore.getScore();
|
||||
|
||||
categoryResult.rules = rules;
|
||||
results.categories[categoryName] = categoryResult;
|
||||
}
|
||||
|
||||
|
||||
// Calculate general score
|
||||
var globalScore = new ScoreMerger();
|
||||
|
||||
for (categoryName in profile.globalScore) {
|
||||
weight = profile.globalScore[categoryName];
|
||||
|
||||
if (results.categories[categoryName]) {
|
||||
globalScore.push(results.categories[categoryName].categoryScore, weight);
|
||||
}
|
||||
}
|
||||
|
||||
results.globalScore = Math.round(globalScore.getScore());
|
||||
|
||||
|
||||
if (debugFlag) {
|
||||
debug('Score calculation finished:');
|
||||
debug(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
|
||||
var ScoreMerger = function() {
|
||||
var sum = 0;
|
||||
var totalWeight = 0;
|
||||
|
||||
this.push = function(score, weight) {
|
||||
sum += (100 - score) * weight;
|
||||
totalWeight += weight;
|
||||
};
|
||||
|
||||
this.getScore = function() {
|
||||
if (totalWeight === 0) {
|
||||
return 100;
|
||||
}
|
||||
return Math.round(100 - (sum / totalWeight));
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ScoreCalculator();
|
||||
@@ -0,0 +1,108 @@
|
||||
var debug = require('debug')('ylt:screenshotHandlerAgent');
|
||||
var Jimp = require('jimp');
|
||||
var Q = require('q');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
|
||||
var screenshotHandler = function() {
|
||||
|
||||
this.findAndOptimizeScreenshot = function(tmpScreenshotPath, width) {
|
||||
var that = this;
|
||||
|
||||
debug('Starting screenshot transformation');
|
||||
|
||||
return this.openImage(tmpScreenshotPath)
|
||||
|
||||
.then(function(image) {
|
||||
that.deleteTmpFile(tmpScreenshotPath);
|
||||
return that.resizeImage(image, width);
|
||||
})
|
||||
|
||||
.then(this.toBuffer);
|
||||
};
|
||||
|
||||
|
||||
this.openImage = function(imagePath) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
Jimp.read(imagePath, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not open imagePath %s', imagePath);
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly open');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.resizeImage = function(image, newWidth) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
var currentWidth = image.bitmap.width;
|
||||
|
||||
if (currentWidth > 0) {
|
||||
var ratio = newWidth / currentWidth;
|
||||
|
||||
image.scale(ratio, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not resize image');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly resized');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.reject('Could not resize an empty image');
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.toBuffer = function(image) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
|
||||
if (err) {
|
||||
debug('Could not save image to buffer');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly transformed to buffer');
|
||||
deferred.resolve(buffer);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.deleteTmpFile = function(tmpFilePath) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
fs.unlink(tmpFilePath, function (err) {
|
||||
if (err) {
|
||||
debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
|
||||
} else {
|
||||
debug('Screenshot temporary file deleted.');
|
||||
}
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new screenshotHandler();
|
||||
@@ -0,0 +1,92 @@
|
||||
var debug = require('debug')('ylt:colorDiff');
|
||||
var diff = require('color-diff');
|
||||
var parse = require('parse-color');
|
||||
|
||||
|
||||
var colorDiff = function() {
|
||||
|
||||
var COLOR_DIFF_THRESHOLD = 0.7;
|
||||
|
||||
this.compareAllColors = function(data) {
|
||||
debug('Starting to compare all colors...');
|
||||
|
||||
var offenders = data.toolsResults.phantomas.offenders.cssColors;
|
||||
var colors = (offenders) ? this.parseAllColors(offenders) : [];
|
||||
var result = [];
|
||||
|
||||
// Compare colors to each other
|
||||
for (var i = 0; i < colors.length - 1 ; i++) {
|
||||
for (var j = i + 1; j < colors.length; j++) {
|
||||
debug('Comparing color %s with color %s', colors[i].original, colors[j].original);
|
||||
if (this.compareTwoColors(colors[i], colors[j])) {
|
||||
debug('Colors are similar!');
|
||||
result.push({
|
||||
color1: colors[i].original,
|
||||
color2: colors[j].original,
|
||||
isDark: (colors[i].Lab.L < 60)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.toolsResults.colorDiff = {
|
||||
metrics: {
|
||||
similarColors: result.length
|
||||
},
|
||||
offenders: {
|
||||
similarColors: result
|
||||
}
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.parseAllColors = function(offenders) {
|
||||
var parsedOffenders = offenders.map(this.parseOffender);
|
||||
var deduplicatedColors = {};
|
||||
|
||||
parsedOffenders.forEach(function(color) {
|
||||
if (color !== null) {
|
||||
deduplicatedColors[color] = color;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(deduplicatedColors).map(this.parseColor);
|
||||
};
|
||||
|
||||
this.parseOffender = function(offender) {
|
||||
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message);
|
||||
return regexResult ? regexResult[1] : null;
|
||||
};
|
||||
|
||||
this.parseColor = function(color) {
|
||||
var colorArray = parse(color).rgba;
|
||||
|
||||
var obj = {
|
||||
R: colorArray[0],
|
||||
G: colorArray[1],
|
||||
B: colorArray[2],
|
||||
A: colorArray[3]
|
||||
};
|
||||
|
||||
obj.Lab = diff.rgb_to_lab(obj);
|
||||
obj.original = color;
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
this.compareTwoColors = function(color1, color2) {
|
||||
if (color1.A !== color2.A) {
|
||||
debug('Comparison not possible, because the alpha channel is different');
|
||||
return false;
|
||||
}
|
||||
|
||||
var delta = diff.diff(color1.Lab, color2.Lab);
|
||||
debug('Delta is %d', delta);
|
||||
|
||||
return delta <= COLOR_DIFF_THRESHOLD;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = new colorDiff();
|
||||
@@ -0,0 +1,53 @@
|
||||
var debug = require('debug')('ylt:domAccessAgregator');
|
||||
|
||||
var domAccessAgregator = function() {
|
||||
'use strict';
|
||||
|
||||
this.agregate = function(data) {
|
||||
debug('Starting to agregate DOM Accesses...');
|
||||
|
||||
let count = 0;
|
||||
let offenders = {
|
||||
byType: {}
|
||||
};
|
||||
|
||||
const metricsToGather = [
|
||||
'DOMqueriesById',
|
||||
'DOMqueriesByTagName',
|
||||
'DOMqueriesByClassName',
|
||||
'DOMqueriesByQuerySelectorAll',
|
||||
'DOMinserts',
|
||||
'DOMmutationsInserts',
|
||||
'DOMmutationsRemoves',
|
||||
'DOMmutationsAttributes',
|
||||
'eventsBound'
|
||||
];
|
||||
|
||||
metricsToGather.forEach(key => {
|
||||
|
||||
if (data.toolsResults.phantomas.metrics[key]) {
|
||||
count += data.toolsResults.phantomas.metrics[key];
|
||||
}
|
||||
|
||||
offenders.byType[key] = [];
|
||||
if (data.toolsResults.phantomas.offenders[key]) {
|
||||
offenders.byType[key] = data.toolsResults.phantomas.offenders[key];
|
||||
}
|
||||
});
|
||||
|
||||
data.toolsResults.domAccessAgregator = {
|
||||
metrics: {
|
||||
DOMaccesses: count
|
||||
},
|
||||
offenders: {
|
||||
DOMaccesses: offenders
|
||||
}
|
||||
};
|
||||
|
||||
debug('Done agregating DOM Accesses.');
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new domAccessAgregator();
|
||||
@@ -0,0 +1,168 @@
|
||||
var debug = require('debug')('ylt:mediaQueriesChecker');
|
||||
var parseMediaQuery = require('css-mq-parser');
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
|
||||
|
||||
var mediaQueriesChecker = function() {
|
||||
'use strict';
|
||||
|
||||
var MOBILE_MIN_BREAKPOINT = 200;
|
||||
var MOBILE_MAX_BREAKPOINT = 300;
|
||||
|
||||
this.analyzeMediaQueries = function(data) {
|
||||
debug('Starting to check all media queries...');
|
||||
|
||||
var offenders = data.toolsResults.phantomas.offenders.cssMediaQueries;
|
||||
var mediaQueries = (offenders) ? this.parseAllMediaQueries(offenders) : [];
|
||||
|
||||
var notMobileFirstCount = 0;
|
||||
var notMobileFirstOffenders = [];
|
||||
|
||||
var breakpointsOffenders = {};
|
||||
|
||||
for (var i = 0; i < mediaQueries.length; i++) {
|
||||
var item = mediaQueries[i];
|
||||
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.isForMobile) {
|
||||
notMobileFirstCount += item.mediaQuery.rules;
|
||||
notMobileFirstOffenders.push(item.mediaQuery);
|
||||
}
|
||||
|
||||
for (var j = 0; j < item.breakpoints.length; j++) {
|
||||
var breakpointString = item.breakpoints[j].string;
|
||||
if (!breakpointsOffenders[breakpointString]) {
|
||||
breakpointsOffenders[breakpointString] = {
|
||||
count: 1,
|
||||
pixels: item.breakpoints[j].pixels
|
||||
};
|
||||
} else {
|
||||
breakpointsOffenders[breakpointString].count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.toolsResults.mediaQueriesChecker = {
|
||||
metrics: {
|
||||
cssMobileFirst: notMobileFirstCount,
|
||||
cssBreakpoints: Object.keys(breakpointsOffenders).length
|
||||
},
|
||||
offenders: {
|
||||
cssMobileFirst: notMobileFirstOffenders,
|
||||
cssBreakpoints: breakpointsOffenders
|
||||
}
|
||||
};
|
||||
|
||||
debug('End of media queries check');
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.parseAllMediaQueries = function(offenders) {
|
||||
return offenders.map(this.parseOneMediaQuery);
|
||||
};
|
||||
|
||||
this.parseOneMediaQuery = function(offender) {
|
||||
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
|
||||
var parts = /^@media (.*) \((\d+ rules)\)$/.exec(splittedOffender.css);
|
||||
|
||||
if (!parts) {
|
||||
debug('Failed to parse media query ' + offender);
|
||||
return false;
|
||||
}
|
||||
|
||||
var rulesCount = parseInt(parts[2], 10);
|
||||
var query = parts[1];
|
||||
|
||||
var isForMobile = false;
|
||||
var breakpoints = [];
|
||||
|
||||
try {
|
||||
|
||||
var ast = parseMediaQuery(query);
|
||||
|
||||
var min = 0;
|
||||
var max = Infinity;
|
||||
var pixels;
|
||||
|
||||
ast.forEach(function(astItem) {
|
||||
astItem.expressions.forEach(function(expression) {
|
||||
if (expression.feature === 'width' || expression.feature === 'device-width') {
|
||||
if (astItem.inverse === false) {
|
||||
if (expression.modifier === 'max') {
|
||||
pixels = toPixels(expression.value);
|
||||
max = Math.min(max, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
} else if (expression.modifier === 'min') {
|
||||
pixels = toPixels(expression.value);
|
||||
min = Math.max(min, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
}
|
||||
} else if (astItem.inverse === true) {
|
||||
if (expression.modifier === 'max') {
|
||||
pixels = toPixels(expression.value);
|
||||
min = Math.max(min, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
} else if (expression.modifier === 'min') {
|
||||
pixels = toPixels(expression.value);
|
||||
max = Math.min(max, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
isForMobile = (min <= MOBILE_MIN_BREAKPOINT && max >= MOBILE_MAX_BREAKPOINT && max !== Infinity);
|
||||
|
||||
} catch(error) {
|
||||
debug('Failed to parse media query ' + offender);
|
||||
}
|
||||
|
||||
return {
|
||||
mediaQuery: {
|
||||
query: query,
|
||||
rules: rulesCount,
|
||||
file: splittedOffender.file,
|
||||
line: splittedOffender.line,
|
||||
column: splittedOffender.column
|
||||
},
|
||||
isForMobile: isForMobile,
|
||||
breakpoints: breakpoints
|
||||
};
|
||||
};
|
||||
|
||||
// Parses a size in em, pt (or px) and returns it in px
|
||||
function toPixels(size) {
|
||||
var splittedSize = /^([\d\.]+)(.*)/.exec(size);
|
||||
var value = parseFloat(splittedSize[1]);
|
||||
var unit = splittedSize[2];
|
||||
|
||||
if (unit === 'em') {
|
||||
return value * 16;
|
||||
}
|
||||
|
||||
if (unit === 'pt') {
|
||||
return value / 12 * 16;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new mediaQueriesChecker();
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Lists the number of different characters
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '1.0';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
//phantomas.setMetric('differentCharacters'); // @desc the number of different characters in the body @offenders
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log("charactersCount: starting to analyze characters on page...");
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.spyEnabled(false, 'analyzing characters on page');
|
||||
|
||||
function getLetterCount(arr){
|
||||
return arr.reduce(function(prev, next){
|
||||
prev[next] = 1;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (document.body && document.body.textContent) {
|
||||
var allText = '';
|
||||
|
||||
// Traverse all DOM Tree to read text
|
||||
var runner = new phantomas.nodeRunner();
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
|
||||
// Grabing text nodes
|
||||
case Node.TEXT_NODE:
|
||||
if (node.parentNode.tagName !== 'SCRIPT' && node.parentNode.tagName !== 'STYLE') {
|
||||
allText += node.nodeValue;
|
||||
}
|
||||
break;
|
||||
|
||||
// Grabing CSS content properties
|
||||
case Node.ELEMENT_NODE:
|
||||
if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {
|
||||
allText += window.getComputedStyle(node).getPropertyValue('content');
|
||||
allText += window.getComputedStyle(node, ':before').getPropertyValue('content');
|
||||
allText += window.getComputedStyle(node, ':after').getPropertyValue('content');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Reduce all text found in page into a string of unique chars
|
||||
var charactersList = getLetterCount(allText.split(''));
|
||||
var charsetString = Object.keys(charactersList).sort().join('');
|
||||
|
||||
// Remove blank characters
|
||||
charsetString = charsetString.replace(/\s/g, '');
|
||||
|
||||
phantomas.setMetric('differentCharacters', charsetString.length);
|
||||
phantomas.addOffender('differentCharacters', charsetString);
|
||||
}
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
}(window.__phantomas));
|
||||
});
|
||||
|
||||
phantomas.log("charactersCount: analyzing characters done.");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Push items and count them
|
||||
*/
|
||||
|
||||
function collection() {
|
||||
/* jshint validthis: true */
|
||||
this.items = {};
|
||||
}
|
||||
|
||||
collection.prototype = {
|
||||
push: function(item) {
|
||||
if (typeof this.items[item] === 'undefined') {
|
||||
this.items[item] = {
|
||||
cnt: 1
|
||||
};
|
||||
} else {
|
||||
this.items[item].cnt++;
|
||||
}
|
||||
},
|
||||
|
||||
sort: function() {
|
||||
var newItems = {},
|
||||
sortedKeys;
|
||||
|
||||
// sort in descending order (by cnt)
|
||||
sortedKeys = Object.keys(this.items).sort((function(a, b) {
|
||||
return this.items[b].cnt - this.items[a].cnt;
|
||||
}).bind(this));
|
||||
|
||||
// build new items dictionary
|
||||
sortedKeys.forEach(function(key) {
|
||||
newItems[key] = this.items[key];
|
||||
}, this);
|
||||
|
||||
this.items = newItems;
|
||||
return this;
|
||||
},
|
||||
|
||||
forEach: function(callback) {
|
||||
Object.keys(this.items).forEach(function(key) {
|
||||
callback(key, this.items[key].cnt);
|
||||
}, this);
|
||||
},
|
||||
|
||||
has: function(item) {
|
||||
return (typeof this.items[item] !== 'undefined');
|
||||
},
|
||||
|
||||
getLength: function() {
|
||||
return Object.keys(this.items).length;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = collection;
|
||||
@@ -0,0 +1,107 @@
|
||||
var Q = require('q');
|
||||
var path = require('path');
|
||||
var debug = require('debug')('ylt:phantomaswrapper');
|
||||
var phantomas = require('phantomas');
|
||||
|
||||
|
||||
var PhantomasWrapper = function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This is the phantomas launcher. It merges user chosen options into the default options
|
||||
*/
|
||||
this.execute = function(data) {
|
||||
|
||||
var deferred = Q.defer();
|
||||
var task = data.params;
|
||||
|
||||
var viewportOption = null;
|
||||
// Setting screen dimensions for desktop devices only.
|
||||
// Phone and tablet dimensions are dealt by Phantomas.
|
||||
if (task.options.device === 'desktop') {
|
||||
// Similar to an old non-retina Macbook Air 13"
|
||||
viewportOption = '1280x800x1';
|
||||
} else if (task.options.device === 'desktop-hd') {
|
||||
// Similar to a retina Macbook Pro 16"
|
||||
viewportOption = '1536x960x2';
|
||||
}
|
||||
|
||||
var options = {
|
||||
|
||||
// Cusomizable options
|
||||
'timeout': task.options.timeout || 120,
|
||||
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/85.0.4183.121 Safari/537.36' : null,
|
||||
'tablet': (task.options.device === 'tablet'),
|
||||
'phone': (task.options.device === 'phone'),
|
||||
'screenshot': task.options.screenshot || false,
|
||||
'viewport': viewportOption,
|
||||
'wait-for-network-idle': true,
|
||||
'cookie': task.options.cookie,
|
||||
'auth-user': task.options.authUser,
|
||||
'auth-pass': task.options.authPass,
|
||||
'block-domain': task.options.blockDomain,
|
||||
'allow-domain': task.options.allowDomain,
|
||||
'no-externals': task.options.noExternals,
|
||||
'local-storage': task.options.localStorage,
|
||||
'session-storage': task.options.sessionStorage,
|
||||
|
||||
// Mandatory
|
||||
'analyze-css': true,
|
||||
'analyze-images': true,
|
||||
'ignoreSslErrors': true, // until Phantomas 2.1
|
||||
'ignore-ssl-errors': true // for Phantomas >= 2.2
|
||||
};
|
||||
|
||||
|
||||
// Proxy option can't be set to null or undefined...
|
||||
// this is why it's set now and not in the object above
|
||||
if (task.options.proxy) {
|
||||
options.proxy = task.options.proxy;
|
||||
}
|
||||
|
||||
var debugCmd = 'DEBUG=* node node_modules/phantomas/bin/phantomas.js --url ' + task.url;
|
||||
Object.keys(options).forEach(function(key) {
|
||||
if (key !== 'wait-for-network-idle' && options[key] !== null) {
|
||||
debugCmd += ' --' + key + ' ' + options[key];
|
||||
}
|
||||
});
|
||||
debug('If you want to run Phantomas alone for debugging purpose, this is the command: %s', debugCmd);
|
||||
|
||||
// It's time to launch the test!!!
|
||||
|
||||
const promise = phantomas(task.url, options);
|
||||
|
||||
// handle the promise
|
||||
promise.
|
||||
then(results => {
|
||||
var json = {
|
||||
generator: results.getGenerator(),
|
||||
url: results.getUrl(),
|
||||
metrics: results.getMetrics(),
|
||||
offenders: results.getAllOffenders()
|
||||
};
|
||||
|
||||
// Special rules here
|
||||
if (task.options.device !== 'phone') {
|
||||
delete json.metrics.imagesExcessiveDensity;
|
||||
delete json.offenders.imagesExcessiveDensity;
|
||||
}
|
||||
|
||||
deferred.resolve(json);
|
||||
}).
|
||||
catch(res => {
|
||||
console.error(res);
|
||||
deferred.reject('Phantomas failed: ' + res.message);
|
||||
});
|
||||
|
||||
promise.on('milestone', function(event) {
|
||||
if (event === 'domReady' || event === 'domComplete') {
|
||||
deferred.notify(event);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new PhantomasWrapper();
|
||||
@@ -0,0 +1,107 @@
|
||||
var debug = require('debug')('ylt:brotliCompressor');
|
||||
|
||||
var Q = require('q');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var gzipCompressor = require('./gzipCompressor');
|
||||
|
||||
|
||||
var GzipCompressor = function() {
|
||||
|
||||
var BROTLI_COMPRESSION_LEVEL = 9;
|
||||
|
||||
function compressFile(entry) {
|
||||
debug('Entering brotli compressor');
|
||||
return brotlifyFile(entry)
|
||||
|
||||
.then(brotliOptimizedFile);
|
||||
}
|
||||
|
||||
// Compress with Brotli files that were not compressed or not with brotli (with gzip or deflate)
|
||||
function brotlifyFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.bodyBuffer) {
|
||||
|
||||
if (!entry.weightCheck.isCompressed) {
|
||||
debug('File %s was not compressed at all, trying Brotli over it.', entry.url);
|
||||
} else if (entry.weightCheck.compressionTool !== 'brotli') {
|
||||
debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool);
|
||||
}
|
||||
|
||||
zlib.brotliCompress(entry.weightCheck.bodyBuffer, {
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
|
||||
}
|
||||
}, function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress uncompressed file with brotli');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
if (!entry.weightCheck.isCompressed) {
|
||||
debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
|
||||
} else if (entry.weightCheck.compressionTool !== 'brotli') {
|
||||
debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
|
||||
}
|
||||
|
||||
entry.weightCheck.afterBrotliCompression = compressedSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
debug('Compression not needed');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Gzip a file after minification or optimization if this step was successful
|
||||
function brotliOptimizedFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
|
||||
debug('Trying to brotlify file after minification: %s', entry.url);
|
||||
|
||||
var uncompressedSize = entry.weightCheck.optimized;
|
||||
|
||||
zlib.brotliCompress(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), {
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
|
||||
}
|
||||
}, function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress minified file with brotli');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
debug('Compressing optimized file not needed');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return {
|
||||
compressFile: compressFile
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new GzipCompressor();
|
||||
@@ -0,0 +1,250 @@
|
||||
var debug = require('debug')('ylt:contentTypeChecker');
|
||||
var Q = require('q');
|
||||
var isJpg = require('is-jpg');
|
||||
var isPng = require('is-png');
|
||||
var isSvg = require('is-svg');
|
||||
var isGif = require('is-gif');
|
||||
var isWebp = require('is-webp');
|
||||
var isWoff = require('is-woff');
|
||||
var isWoff2 = require('is-woff2');
|
||||
var isOtf = require('is-otf');
|
||||
var isTtf = require('is-ttf');
|
||||
var isEot = require('is-eot');
|
||||
var isJson = require('is-json');
|
||||
|
||||
var ContentTypeChecker = function() {
|
||||
|
||||
function checkContentType(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Setting isSomething values:
|
||||
switch(entry.type) {
|
||||
case 'html':
|
||||
entry.isHTML = true;
|
||||
break;
|
||||
case 'xml':
|
||||
entry.isXML = true;
|
||||
break;
|
||||
case 'css':
|
||||
entry.isCSS = true;
|
||||
break;
|
||||
case 'js':
|
||||
entry.isJS = true;
|
||||
break;
|
||||
case 'json':
|
||||
entry.isJSON = true;
|
||||
break;
|
||||
case 'image':
|
||||
entry.isImage = true;
|
||||
break;
|
||||
case 'webfont':
|
||||
entry.isWebFont = true;
|
||||
break;
|
||||
case 'video':
|
||||
entry.isVideo = true;
|
||||
break;
|
||||
case 'favicon':
|
||||
entry.isFavicon = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Now let's check for mistakes by analysing body content. It happens more often then we think!
|
||||
|
||||
// Ignore very small files as they are generally tracking pixels
|
||||
if (entry.weightCheck && entry.weightCheck.bodyBuffer && entry.weightCheck.bodySize > 100) {
|
||||
var foundType;
|
||||
|
||||
try {
|
||||
foundType = findContentType(entry.weightCheck.bodyBuffer);
|
||||
|
||||
// If it's an image or a font, then rewrite.
|
||||
if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
|
||||
if (foundType.type !== entry.type) {
|
||||
debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
|
||||
}
|
||||
rewriteContentType(entry, foundType);
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
debug('Error while analyzing the contentType of %s', entry.url);
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
deferred.resolve(entry);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function findContentType(bodyBuffer) {
|
||||
var bodyStr = bodyBuffer.toString();
|
||||
|
||||
if (isJpg(bodyBuffer)) {
|
||||
return contentTypes.jpeg;
|
||||
}
|
||||
|
||||
if (isPng(bodyBuffer)) {
|
||||
return contentTypes.png;
|
||||
}
|
||||
|
||||
// https://github.com/sindresorhus/is-svg/issues/7
|
||||
if (/<svg/.test(bodyStr) && isSvg(bodyStr)) {
|
||||
return contentTypes.svg;
|
||||
}
|
||||
|
||||
if (isGif(bodyBuffer)) {
|
||||
return contentTypes.gif;
|
||||
}
|
||||
|
||||
if (isWebp(bodyBuffer)) {
|
||||
return contentTypes.webp;
|
||||
}
|
||||
|
||||
if (isWoff(bodyBuffer)) {
|
||||
return contentTypes.woff;
|
||||
}
|
||||
|
||||
if (isWoff2(bodyBuffer)) {
|
||||
return contentTypes.woff2;
|
||||
}
|
||||
|
||||
if (isOtf(bodyBuffer)) {
|
||||
return contentTypes.otf;
|
||||
}
|
||||
|
||||
if (isTtf(bodyBuffer)) {
|
||||
return contentTypes.ttf;
|
||||
}
|
||||
|
||||
if (isEot(bodyBuffer)) {
|
||||
return contentTypes.eot;
|
||||
}
|
||||
|
||||
if (isJson(bodyStr)) {
|
||||
return contentTypes.json;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function rewriteContentType(entry, contentTypeObj) {
|
||||
delete(entry.isHTML);
|
||||
delete(entry.isXML);
|
||||
delete(entry.isCSS);
|
||||
delete(entry.isJS);
|
||||
delete(entry.isJSON);
|
||||
delete(entry.isImage);
|
||||
delete(entry.isSVG);
|
||||
delete(entry.isVideo);
|
||||
delete(entry.isWebFont);
|
||||
delete(entry.isTTF);
|
||||
delete(entry.isFavicon);
|
||||
|
||||
entry.contentType = contentTypeObj.mimes[0];
|
||||
contentTypeObj.updateFn(entry);
|
||||
}
|
||||
|
||||
var contentTypes = {
|
||||
jpeg: {
|
||||
type: 'image',
|
||||
mimes: ['image/jpeg'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'image';
|
||||
entry.isImage = true;
|
||||
}
|
||||
},
|
||||
png: {
|
||||
type: 'image',
|
||||
mimes: ['image/png'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'image';
|
||||
entry.isImage = true;
|
||||
}
|
||||
},
|
||||
svg: {
|
||||
type: 'image',
|
||||
mimes: ['image/svg+xml'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'image';
|
||||
entry.isImage = true;
|
||||
entry.isSVG = true;
|
||||
}
|
||||
},
|
||||
gif: {
|
||||
type: 'image',
|
||||
mimes: ['image/gif'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'image';
|
||||
entry.isImage = true;
|
||||
}
|
||||
},
|
||||
webp: {
|
||||
type: 'image',
|
||||
mimes: ['image/webp'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'image';
|
||||
entry.isImage = true;
|
||||
}
|
||||
},
|
||||
woff: {
|
||||
type: 'webfont',
|
||||
mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'webfont';
|
||||
entry.isWebFont = true;
|
||||
entry.isWoff = true;
|
||||
}
|
||||
},
|
||||
woff2: {
|
||||
type: 'webfont',
|
||||
mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'webfont';
|
||||
entry.isWebFont = true;
|
||||
entry.isWoff2 = true;
|
||||
}
|
||||
},
|
||||
otf: {
|
||||
type: 'webfont',
|
||||
mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'webfont';
|
||||
entry.isWebFont = true;
|
||||
}
|
||||
},
|
||||
ttf: {
|
||||
type: 'webfont',
|
||||
mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'webfont';
|
||||
entry.isWebFont = true;
|
||||
entry.isTTF = true;
|
||||
}
|
||||
},
|
||||
eot: {
|
||||
type: 'webfont',
|
||||
mimes: ['application/vnd.ms-fontobject', 'font/eot'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'webfont';
|
||||
entry.isWebFont = true;
|
||||
}
|
||||
},
|
||||
json: {
|
||||
type: 'json',
|
||||
mimes: ['application/json'],
|
||||
updateFn: function(entry) {
|
||||
entry.type = 'json';
|
||||
entry.isJSON = true;
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
checkContentType: checkContentType,
|
||||
findContentType: findContentType
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ContentTypeChecker();
|
||||
@@ -0,0 +1,256 @@
|
||||
var debug = require('debug')('ylt:fileMinifier');
|
||||
|
||||
var Q = require('q');
|
||||
var UglifyJS = require('uglify-js');
|
||||
var CleanCSS = require('clean-css');
|
||||
var htmlMinifier = require('html-minifier');
|
||||
|
||||
|
||||
var FileMinifier = function() {
|
||||
|
||||
function minifyFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
var bodyString = entry.weightCheck.bodyBuffer.toString();
|
||||
|
||||
debug('Let\'s try to optimize %s', entry.url);
|
||||
debug('Current file size is %d', fileSize);
|
||||
var startTime = Date.now();
|
||||
|
||||
if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(bodyString)) {
|
||||
|
||||
debug('File is a JS');
|
||||
|
||||
return minifyJs(bodyString)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('JS minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (entry.isCSS) {
|
||||
|
||||
debug('File is a CSS');
|
||||
|
||||
return minifyCss(entry.weightCheck.bodyBuffer.toString())
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('CSS minification took %dms', endTime - startTime);
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('CSS minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (entry.isHTML) {
|
||||
|
||||
debug('File is an HTML');
|
||||
|
||||
return minifyHtml(entry.weightCheck.bodyBuffer.toString())
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('HTML minification took %dms', endTime - startTime);
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('HTML minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else {
|
||||
debug('Not minifiable type or already minified');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 2KB or over 20%,
|
||||
// but it's ignored if is below 400 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2096 || (ratio > 0.2 && gain > 400));
|
||||
}
|
||||
|
||||
// Uglify
|
||||
function minifyJs(body) {
|
||||
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
var result = UglifyJS.minify(body, {
|
||||
// Only do the compression step for smaller files
|
||||
// otherwise it can take a very long time compared to the gain
|
||||
compress: (body.length < 200*1024)
|
||||
});
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify took %dms', endTime - startTime);
|
||||
deferred.resolve(result.code);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Clear-css
|
||||
function minifyCss(body) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
try {
|
||||
var result = new CleanCSS().minify(body);
|
||||
deferred.resolve(result.styles);
|
||||
} catch(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// HTMLMinifier
|
||||
function minifyHtml(body) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
try {
|
||||
var result = htmlMinifier.minify(body, {
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
continueOnParseError: true,
|
||||
decodeEntities: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
preserveLineBreaks: true,
|
||||
removeAttributeQuotes: true,
|
||||
removeComments: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true
|
||||
});
|
||||
deferred.resolve(result);
|
||||
} catch(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Avoid losing time trying to compress some JS libraries known as already compressed
|
||||
function isKnownAsMinified(url) {
|
||||
var result = false;
|
||||
|
||||
// Twitter
|
||||
result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
|
||||
|
||||
// Facebook
|
||||
result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
|
||||
|
||||
// Google +1
|
||||
result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
|
||||
|
||||
// jQuery CDN
|
||||
result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
|
||||
|
||||
// Google Analytics
|
||||
result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
|
||||
|
||||
if (result === true) {
|
||||
debug('This file is known as already minified. Skipping minification: %s', url);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Avoid losing time trying to compress JS files if they already look minified
|
||||
// by counting the number of lines compared to the total size.
|
||||
// Less than 2KB per line is suspicious
|
||||
function looksAlreadyMinified(code) {
|
||||
var linesCount = code.split(/\r\n|\r|\n/).length;
|
||||
var linesRatio = code.length / linesCount;
|
||||
var looksMinified = linesRatio > 2 * 1024;
|
||||
|
||||
debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
|
||||
debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
|
||||
|
||||
return looksMinified;
|
||||
}
|
||||
|
||||
function entryTypeCanBeMinified(entry) {
|
||||
return entry.isJS || entry.isCSS || entry.isHTML;
|
||||
}
|
||||
|
||||
return {
|
||||
minifyFile: minifyFile,
|
||||
minifyJs: minifyJs,
|
||||
minifyCss: minifyCss,
|
||||
minifyHtml: minifyHtml,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeMinified: entryTypeCanBeMinified,
|
||||
isKnownAsMinified: isKnownAsMinified
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new FileMinifier();
|
||||
@@ -0,0 +1,470 @@
|
||||
var debug = require('debug')('ylt:fontAnalyzer');
|
||||
|
||||
var Q = require('q');
|
||||
var fontkit = require('fontkit');
|
||||
var woffTools = require('woff-tools');
|
||||
var ttf2woff2 = require('ttf2woff2');
|
||||
|
||||
var FontAnalyzer = function() {
|
||||
|
||||
function analyzeFont(entry, charsListOnPage) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
if (entry.isWebFont) {
|
||||
debug('File %s is a font. Let\'s have a look inside!', entry.url);
|
||||
|
||||
convertToWoff2(entry)
|
||||
|
||||
.then(function(entry) {
|
||||
return getMetricsFromFont(entry, charsListOnPage);
|
||||
})
|
||||
|
||||
.then(function(fontMetrics) {
|
||||
entry.fontMetrics = fontMetrics;
|
||||
})
|
||||
|
||||
.fail(function(error) {
|
||||
debug('Could not open the font: %s', error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
deferred.resolve(entry);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function convertToWoff2(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
debug('Entering font format converter...');
|
||||
|
||||
var fileSize = entry.weightCheck.bodySize;
|
||||
var ttf;
|
||||
var woff2;
|
||||
var newFileSize;
|
||||
|
||||
if (entry.isWoff2) {
|
||||
|
||||
debug('File is already a woff2.');
|
||||
deferred.resolve(entry);
|
||||
|
||||
} else if (fileSize > 1024 * 1024) {
|
||||
|
||||
// Don't try to convert huge font files, it would block the server for several minutes
|
||||
debug('Font is too big, skipping conversion.');
|
||||
deferred.resolve(entry);
|
||||
|
||||
} else if (entry.isWoff) {
|
||||
|
||||
debug('File is a woff. Let\'s convert to woff2');
|
||||
|
||||
try {
|
||||
|
||||
debug('Current file size is %d', fileSize);
|
||||
|
||||
ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer);
|
||||
woff2 = ttf2woff2(ttf);
|
||||
newFileSize = woff2.length;
|
||||
|
||||
debug('New font size is %d', newFileSize);
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
entry.weightCheck.sizeAsWoff2 = newFileSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
|
||||
} catch(error) {
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
} else if (entry.isTTF) {
|
||||
|
||||
debug('File is a TTF. Let\'s convert to woff2');
|
||||
|
||||
try {
|
||||
|
||||
debug('Current file size is %d', fileSize);
|
||||
|
||||
woff2 = ttf2woff2(entry.weightCheck.bodyBuffer);
|
||||
newFileSize = woff2.length;
|
||||
|
||||
debug('New image size is %d', newFileSize);
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
entry.weightCheck.sizeAsWoff2 = newFileSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
|
||||
} catch(error) {
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Other font formats are not handled
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 1KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function getMetricsFromFont(entry, charsListOnPage) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
try {
|
||||
var startTime = Date.now();
|
||||
var font = fontkit.create(entry.weightCheck.bodyBuffer);
|
||||
|
||||
var result = {
|
||||
name: font.fullName || font.postscriptName || font.familyName,
|
||||
numGlyphs: font.numGlyphs,
|
||||
averageGlyphComplexity: getAverageGlyphComplexity(font),
|
||||
compressedWeight: entry.weightCheck.afterGzipCompression || entry.weightCheck.bodySize,
|
||||
unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
|
||||
numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
|
||||
};
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Font analysis took %dms', endTime - startTime);
|
||||
|
||||
// Mark fonts that are not used on the page (#224)
|
||||
var fontIsUsed = false;
|
||||
for (var range in result.unicodeRanges) {
|
||||
if (result.unicodeRanges[range].numGlyphsInCommonWithPageContent > 0) {
|
||||
fontIsUsed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result.isUsed = fontIsUsed;
|
||||
|
||||
deferred.resolve(result);
|
||||
} catch(error) {
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Reads the number of vector commands (complexity) needed to render glyphs and
|
||||
// returns the average. Only first 100 glyphes are tested, otherwise it would take tool long;
|
||||
function getAverageGlyphComplexity(font) {
|
||||
var max = Math.min(font.numGlyphs, 100);
|
||||
|
||||
var totalPathsCommands = 0;
|
||||
for (var i = 0; i < max; i++) {
|
||||
totalPathsCommands += font.getGlyph(i).path.commands.length;
|
||||
}
|
||||
return Math.round(totalPathsCommands / max * 10) / 10;
|
||||
}
|
||||
|
||||
function readUnicodeRanges(charsetInFont, charsListOnPage) {
|
||||
var ranges = {};
|
||||
|
||||
// Assign a range to each char found in the font
|
||||
charsetInFont.forEach(function(char) {
|
||||
|
||||
var currentRange = getUnicodeRangeFromChar(char);
|
||||
var currentRangeName = currentRange.name;
|
||||
|
||||
if (!ranges[currentRangeName]) {
|
||||
// Cloning the object
|
||||
ranges[currentRangeName] = Object.assign({}, currentRange);
|
||||
}
|
||||
|
||||
if (!ranges[currentRangeName].charset) {
|
||||
ranges[currentRangeName].charset = '';
|
||||
}
|
||||
|
||||
ranges[currentRangeName].charset += String.fromCharCode(char);
|
||||
});
|
||||
|
||||
var range;
|
||||
var expectedLength;
|
||||
var actualLength;
|
||||
|
||||
for (var rangeName in ranges) {
|
||||
/*jshint loopfunc: true */
|
||||
|
||||
range = ranges[rangeName];
|
||||
|
||||
// Estimate if range is used, based on the characters found in the page
|
||||
range.numGlyphsInCommonWithPageContent = countPossiblyUsedGlyphs(range.charset, charsListOnPage);
|
||||
|
||||
// Calculate coverage
|
||||
if (rangeName !== 'Others') {
|
||||
expectedLength = range.rangeEnd - range.rangeStart + 1;
|
||||
actualLength = range.charset.length;
|
||||
range.coverage = Math.min(actualLength / expectedLength, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function countPossiblyUsedGlyphs(charsetInFont, charsListOnPage) {
|
||||
var count = 0;
|
||||
charsListOnPage.split('').forEach(function(char) {
|
||||
if (charsetInFont.indexOf(char) >= 0) {
|
||||
count ++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
function getCharacterSetAsString(characterSet) {
|
||||
var str = '';
|
||||
characterSet.forEach(function(charCode) {
|
||||
str += String.fromCharCode(charCode);
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
function getUnicodeRangeFromChar(char) {
|
||||
return UNICODE_RANGES.find(function(range) {
|
||||
return (char >= range.rangeStart && char <= range.rangeEnd);
|
||||
}) || {name: 'Others'};
|
||||
}
|
||||
|
||||
|
||||
var UNICODE_RANGES = [
|
||||
{
|
||||
name: 'Basic Latin',
|
||||
rangeStart: 0x0020,
|
||||
rangeEnd: 0x007F
|
||||
},
|
||||
{
|
||||
name: 'Latin-1 Supplement',
|
||||
rangeStart: 0x00A0,
|
||||
rangeEnd: 0x00FF
|
||||
},
|
||||
{
|
||||
name: 'Latin Extended',
|
||||
rangeStart: 0x0100,
|
||||
rangeEnd: 0x024F
|
||||
},
|
||||
{
|
||||
name: 'IPA Extensions',
|
||||
rangeStart: 0x0250,
|
||||
rangeEnd: 0x02AF
|
||||
},
|
||||
{
|
||||
name: 'Greek and Coptic',
|
||||
rangeStart: 0x0370,
|
||||
rangeEnd: 0x03FF
|
||||
},
|
||||
{
|
||||
name: 'Cyrillic',
|
||||
rangeStart: 0x0400,
|
||||
rangeEnd: 0x052F
|
||||
},
|
||||
{
|
||||
name: 'Armenian',
|
||||
rangeStart: 0x0530,
|
||||
rangeEnd: 0x058F
|
||||
},
|
||||
{
|
||||
name: 'Hebrew',
|
||||
rangeStart: 0x0590,
|
||||
rangeEnd: 0x05FF
|
||||
},
|
||||
{
|
||||
name: 'Arabic',
|
||||
rangeStart: 0x0600,
|
||||
rangeEnd: 0x06FF
|
||||
},
|
||||
{
|
||||
name: 'Syriac',
|
||||
rangeStart: 0x0700,
|
||||
rangeEnd: 0x074F
|
||||
},
|
||||
{
|
||||
name: 'Thaana',
|
||||
rangeStart: 0x0780,
|
||||
rangeEnd: 0x07BF
|
||||
},
|
||||
{
|
||||
name: 'Devanagari',
|
||||
rangeStart: 0x0900,
|
||||
rangeEnd: 0x097F
|
||||
},
|
||||
{
|
||||
name: 'Bengali',
|
||||
rangeStart: 0x0980,
|
||||
rangeEnd: 0x09FF
|
||||
},
|
||||
{
|
||||
name: 'Gurmukhi',
|
||||
rangeStart: 0x0A00,
|
||||
rangeEnd: 0x0A7F
|
||||
},
|
||||
{
|
||||
name: 'Gujarati',
|
||||
rangeStart: 0x0A80,
|
||||
rangeEnd: 0x0AFF
|
||||
},
|
||||
{
|
||||
name: 'Oriya',
|
||||
rangeStart: 0x0B00,
|
||||
rangeEnd: 0x0B7F
|
||||
},
|
||||
{
|
||||
name: 'Tamil',
|
||||
rangeStart: 0x0B80,
|
||||
rangeEnd: 0x0BFF
|
||||
},
|
||||
{
|
||||
name: 'Telugu',
|
||||
rangeStart: 0x0C00,
|
||||
rangeEnd: 0x0C7F
|
||||
},
|
||||
{
|
||||
name: 'Kannada',
|
||||
rangeStart: 0x0C80,
|
||||
rangeEnd: 0x0CFF
|
||||
},
|
||||
{
|
||||
name: 'Malayalam',
|
||||
rangeStart: 0x0D00,
|
||||
rangeEnd: 0x0D7F
|
||||
},
|
||||
{
|
||||
name: 'Sinhala',
|
||||
rangeStart: 0x0D80,
|
||||
rangeEnd: 0x0DFF
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
rangeStart: 0x0E00,
|
||||
rangeEnd: 0x0E7F
|
||||
},
|
||||
{
|
||||
name: 'Lao',
|
||||
rangeStart: 0x0E80,
|
||||
rangeEnd: 0x0EFF
|
||||
},
|
||||
{
|
||||
name: 'Tibetan',
|
||||
rangeStart: 0x0F00,
|
||||
rangeEnd: 0x0FFF
|
||||
},
|
||||
{
|
||||
name: 'Myanmar',
|
||||
rangeStart: 0x1000,
|
||||
rangeEnd: 0x109F
|
||||
},
|
||||
{
|
||||
name: 'Georgian',
|
||||
rangeStart: 0x10A0,
|
||||
rangeEnd: 0x10FF
|
||||
},
|
||||
{
|
||||
name: 'Hangul Jamo',
|
||||
rangeStart: 0x1100,
|
||||
rangeEnd: 0x11FF
|
||||
},
|
||||
{
|
||||
name: 'Ethiopic',
|
||||
rangeStart: 0x1200,
|
||||
rangeEnd: 0x137F
|
||||
},
|
||||
{
|
||||
name: 'Cherokee',
|
||||
rangeStart: 0x13A0,
|
||||
rangeEnd: 0x13FF
|
||||
},
|
||||
{
|
||||
name: 'Unified Canadian Aboriginal Syllabics',
|
||||
rangeStart: 0x1400,
|
||||
rangeEnd: 0x167F
|
||||
},
|
||||
{
|
||||
name: 'Ogham',
|
||||
rangeStart: 0x1680,
|
||||
rangeEnd: 0x169F
|
||||
},
|
||||
{
|
||||
name: 'Runic',
|
||||
rangeStart: 0x16A0,
|
||||
rangeEnd: 0x16FF
|
||||
},
|
||||
{
|
||||
name: 'Tagalog',
|
||||
rangeStart: 0x1700,
|
||||
rangeEnd: 0x171F
|
||||
},
|
||||
{
|
||||
name: 'Hanunoo',
|
||||
rangeStart: 0x1720,
|
||||
rangeEnd: 0x173F
|
||||
},
|
||||
{
|
||||
name: 'Buhid',
|
||||
rangeStart: 0x1740,
|
||||
rangeEnd: 0x175F
|
||||
},
|
||||
{
|
||||
name: 'Tagbanwa',
|
||||
rangeStart: 0x1760,
|
||||
rangeEnd: 0x177F
|
||||
},
|
||||
{
|
||||
name: 'Khmer',
|
||||
rangeStart: 0x1780,
|
||||
rangeEnd: 0x17FF
|
||||
},
|
||||
{
|
||||
name: 'Mongolian',
|
||||
rangeStart: 0x1800,
|
||||
rangeEnd: 0x18AF
|
||||
},
|
||||
{
|
||||
name: 'Limbu',
|
||||
rangeStart: 0x1900,
|
||||
rangeEnd: 0x194F
|
||||
},
|
||||
{
|
||||
name: 'Tai Le',
|
||||
rangeStart: 0x1950,
|
||||
rangeEnd: 0x197F
|
||||
},
|
||||
{
|
||||
name: 'Hiragana',
|
||||
rangeStart: 0x3040,
|
||||
rangeEnd: 0x309F
|
||||
},
|
||||
{
|
||||
name: 'Katakana',
|
||||
rangeStart: 0x30A0,
|
||||
rangeEnd: 0x30FF
|
||||
},
|
||||
{
|
||||
name: 'Bopomofo',
|
||||
rangeStart: 0x3100,
|
||||
rangeEnd: 0x312F
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
analyzeFont: analyzeFont,
|
||||
getMetricsFromFont: getMetricsFromFont,
|
||||
readUnicodeRanges: readUnicodeRanges,
|
||||
getAverageGlyphComplexity: getAverageGlyphComplexity,
|
||||
countPossiblyUsedGlyphs: countPossiblyUsedGlyphs,
|
||||
getCharacterSetAsString: getCharacterSetAsString,
|
||||
getUnicodeRangeFromChar: getUnicodeRangeFromChar
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new FontAnalyzer();
|
||||
@@ -0,0 +1,103 @@
|
||||
var debug = require('debug')('ylt:gzipCompressor');
|
||||
|
||||
var Q = require('q');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var GzipCompressor = function() {
|
||||
|
||||
function compressFile(entry) {
|
||||
debug('Entering gzip compressor');
|
||||
return gzipUncompressedFile(entry)
|
||||
|
||||
.then(gzipOptimizedFile);
|
||||
}
|
||||
|
||||
// Gzip a file if it was not already gziped
|
||||
function gzipUncompressedFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (entryTypeCanBeCompressed(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) {
|
||||
debug('Compression missing, trying to gzip file %s', entry.url);
|
||||
|
||||
var uncompressedSize = entry.weightCheck.uncompressedSize;
|
||||
|
||||
zlib.gzip(entry.weightCheck.bodyBuffer, function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress uncompressed file with gzip');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
if (gainIsEnough(uncompressedSize, compressedSize)) {
|
||||
debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
|
||||
entry.weightCheck.afterGzipCompression = compressedSize;
|
||||
} else {
|
||||
debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
}
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug('Compression not needed');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Gzip a file after minification or optimization if this step was successful
|
||||
function gzipOptimizedFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
|
||||
debug('Trying to gzip file after minification: %s', entry.url);
|
||||
|
||||
var uncompressedSize = entry.weightCheck.optimized;
|
||||
|
||||
zlib.gzip(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress minified file with gzip');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
entry.weightCheck.afterOptimizationAndGzipCompression = compressedSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug('Compressing optimized file not needed');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 1KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function entryTypeCanBeCompressed(entry) {
|
||||
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
|
||||
}
|
||||
|
||||
return {
|
||||
compressFile: compressFile,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeCompressed: entryTypeCanBeCompressed
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new GzipCompressor();
|
||||
@@ -0,0 +1,51 @@
|
||||
var debug = require('debug')('ylt:imageDimensions');
|
||||
var Q = require('q');
|
||||
var sizeOf = require('image-size');
|
||||
|
||||
var ImageDimensions = function() {
|
||||
|
||||
function getDimensions(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
|
||||
if (isJPEG(entry) || isPNG(entry)) {
|
||||
try {
|
||||
var dimensions = sizeOf(entry.weightCheck.bodyBuffer);
|
||||
debug('Image dimensions of %s: %sx%s', entry.url, dimensions.width, dimensions.height);
|
||||
|
||||
entry.imageDimensions = {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height
|
||||
};
|
||||
} catch(err) {
|
||||
debug('Error while checking image dimensions:');
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
deferred.resolve(entry);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function isJPEG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/jpeg';
|
||||
}
|
||||
|
||||
function isPNG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/png';
|
||||
}
|
||||
|
||||
return {
|
||||
getDimensions: getDimensions
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ImageDimensions();
|
||||
@@ -0,0 +1,248 @@
|
||||
var debug = require('debug')('ylt:imageOptimizer');
|
||||
var Q = require('q');
|
||||
var imagemin = require('imagemin');
|
||||
var imageminJpegtran = require('imagemin-jpegtran');
|
||||
var imageminJpegoptim = require('imagemin-jpegoptim');
|
||||
var imageminOptipng = require('imagemin-optipng');
|
||||
var imageminSvgo = require('imagemin-svgo');
|
||||
|
||||
var ImageOptimizer = function() {
|
||||
|
||||
var MAX_JPEG_QUALITY = 85;
|
||||
var OPTIPNG_COMPRESSION_LEVEL = 1;
|
||||
|
||||
function optimizeImage(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
debug('Let\'s try to optimize %s', entry.url);
|
||||
debug('Current file size is %d', fileSize);
|
||||
|
||||
if (isJPEG(entry)) {
|
||||
debug('File is a JPEG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressJpegLosslessly(entry.weightCheck.bodyBuffer)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('JPEG lossless compression complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
|
||||
// Now let's compress lossy to MAX_JPEG_QUALITY
|
||||
return compressJpegLossly(entry.weightCheck.bodyBuffer);
|
||||
})
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('JPEG lossy compression complete for %s', entry.url);
|
||||
|
||||
if (entry.weightCheck.lossless && entry.weightCheck.lossless < newFileSize) {
|
||||
debug('Lossy compression is not as good as lossless compression. Skipping the lossy.');
|
||||
} else if (gainIsEnough(fileSize, newFileSize)) {
|
||||
|
||||
if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
}
|
||||
|
||||
entry.weightCheck.lossy = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (isPNG(entry)) {
|
||||
|
||||
debug('File is a PNG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressPngLosslessly(entry.weightCheck.bodyBuffer)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('PNG lossless compression complete for %s', entry.url);
|
||||
|
||||
debug('Old file size: %d', fileSize);
|
||||
debug('New file size: %d', newFileSize);
|
||||
debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (isSVG(entry)) {
|
||||
|
||||
debug('File is an SVG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressSvgLosslessly(entry.weightCheck.bodyBuffer)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('SVG lossless compression complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile.toString();
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else {
|
||||
debug('File type %s is not an optimizable image', entry.contentType);
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 2KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function isJPEG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/jpeg';
|
||||
}
|
||||
|
||||
function isPNG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/png';
|
||||
}
|
||||
|
||||
function isSVG(entry) {
|
||||
return entry.isImage && entry.isSVG;
|
||||
}
|
||||
|
||||
function compressJpegLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'jpeg', false);
|
||||
}
|
||||
|
||||
function compressJpegLossly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'jpeg', true);
|
||||
}
|
||||
|
||||
function compressPngLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'png', false);
|
||||
}
|
||||
|
||||
function compressSvgLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'svg', false);
|
||||
}
|
||||
|
||||
function imageminLauncher(imageBody, type, lossy) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
|
||||
|
||||
var engine;
|
||||
if (type === 'jpeg' && !lossy) {
|
||||
engine = imageminJpegtran({progressive: true});
|
||||
} else if (type === 'jpeg' && lossy) {
|
||||
engine = imageminJpegoptim({progressive: true, max: MAX_JPEG_QUALITY});
|
||||
} else if (type === 'png' && !lossy) {
|
||||
engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
|
||||
} else if (type === 'svg' && !lossy) {
|
||||
engine = imageminSvgo({plugins: [{
|
||||
name: 'preset-default',
|
||||
params: {overrides: {removeUselessDefs: false}}
|
||||
}]});
|
||||
} else {
|
||||
deferred.reject('No optimization engine found for imagemin');
|
||||
}
|
||||
|
||||
|
||||
imagemin.buffer(imageBody, {plugins: [engine]})
|
||||
|
||||
.then(function(file) {
|
||||
var endTime = Date.now();
|
||||
debug('Optimization for %s took %d ms', type, endTime - startTime);
|
||||
deferred.resolve(file);
|
||||
})
|
||||
|
||||
.catch(function(err) {
|
||||
debug('Optimisation failed:');
|
||||
debug(err);
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function entryTypeCanBeOptimized(entry) {
|
||||
return isJPEG(entry) || isPNG(entry) || isSVG(entry);
|
||||
}
|
||||
|
||||
return {
|
||||
optimizeImage: optimizeImage,
|
||||
compressJpegLosslessly: compressJpegLosslessly,
|
||||
compressJpegLossly: compressJpegLossly,
|
||||
compressPngLosslessly: compressPngLosslessly,
|
||||
compressSvgLosslessly: compressSvgLosslessly,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeOptimized: entryTypeCanBeOptimized
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ImageOptimizer();
|
||||
@@ -1,33 +1,78 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"version": "1.0.0",
|
||||
"version": "3.0.0",
|
||||
"description": "A tool that audits a webpage for performance and front-end quality issues",
|
||||
"license": "GPL-2.0",
|
||||
"author": {
|
||||
"name": "Gaël Métais",
|
||||
"email": "gael@gaelmetais.com",
|
||||
"url": "https://www.gaelmetais.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/gmetais/YellowLabTools.git"
|
||||
"url": "git://github.com/YellowLabTools/YellowLabTools.git"
|
||||
},
|
||||
"bin": {
|
||||
"yellowlabtools": "./bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"phantomas": "1.7.0",
|
||||
"express": "~4.10.1",
|
||||
"async": "~0.9.0",
|
||||
"socket.io": "~1.2.0",
|
||||
"body-parser": "~1.9.2",
|
||||
"compression": "~1.2.0"
|
||||
"async": "2.6.4",
|
||||
"clean-css": "4.2.1",
|
||||
"color-diff": "1.4.0",
|
||||
"css-mq-parser": "0.0.3",
|
||||
"debug": "4.3.4",
|
||||
"easyxml": "2.0.1",
|
||||
"fontkit": "2.0.2",
|
||||
"html-minifier": "4.0.0",
|
||||
"image-size": "1.0.2",
|
||||
"imagemin": "7.0.1",
|
||||
"imagemin-jpegoptim": "7.0.0",
|
||||
"imagemin-jpegtran": "7.0.0",
|
||||
"imagemin-optipng": "8.0.0",
|
||||
"imagemin-svgo": "9.0.0",
|
||||
"is-eot": "1.0.0",
|
||||
"is-gif": "3.0.0",
|
||||
"is-jpg": "2.0.0",
|
||||
"is-json": "2.0.1",
|
||||
"is-otf": "0.1.2",
|
||||
"is-png": "1.1.0",
|
||||
"is-svg": "3.0.0",
|
||||
"is-ttf": "0.2.2",
|
||||
"is-webp": "1.0.1",
|
||||
"is-woff": "1.0.3",
|
||||
"is-woff2": "1.0.0",
|
||||
"jimp": "0.22.8",
|
||||
"md5": "2.3.0",
|
||||
"meow": "5.0.0",
|
||||
"parse-color": "1.0.0",
|
||||
"phantomas": "2.10.0",
|
||||
"q": "1.5.1",
|
||||
"request": "2.88.2",
|
||||
"sharp": "0.32.6",
|
||||
"ttf2woff2": "5.0.0",
|
||||
"uglify-js": "3.17.4",
|
||||
"woff-tools": "0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-contrib-jshint": "^0.10.0",
|
||||
"matchdep": "^0.3.0",
|
||||
"grunt-mocha-test": "^0.12.2",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-copy": "^0.7.0",
|
||||
"grunt-blanket": "^0.0.8",
|
||||
"chai": "^1.9.2",
|
||||
"mocha": "^2.0.1",
|
||||
"phantomjs": "^1.9.10",
|
||||
"grunt-fontsmith": "^0.9.1",
|
||||
"grunt-contrib-less": "^0.12.0"
|
||||
"chai": "~4.3.7",
|
||||
"mocha": "~10.2.0",
|
||||
"sinon": "~15.2.0",
|
||||
"sinon-chai": "3.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
}
|
||||
"test": "mocha './test/core/**.js'"
|
||||
},
|
||||
"keywords": [
|
||||
"performance",
|
||||
"metrics",
|
||||
"webperf",
|
||||
"pagespeed",
|
||||
"budget",
|
||||
"phantomas",
|
||||
"puppeteer"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* Overwritting the original spying functions in scope.js
|
||||
* This is done so we now have a before AND an after callback on the spy
|
||||
*
|
||||
* @see http://code.jquery.com/jquery-1.10.2.js
|
||||
* @see http://code.jquery.com/jquery-2.0.3.js
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
var responseEndTime = Date.now();
|
||||
|
||||
phantomas.on('responseEnd', function() {
|
||||
responseEndTime = Date.now();
|
||||
});
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
|
||||
(function(phantomas) {
|
||||
|
||||
// Overwritting phantomas spy function
|
||||
(function() {
|
||||
var enabled = true;
|
||||
|
||||
// turn off spying to not include internal phantomas actions
|
||||
function spyEnabled(state, reason) {
|
||||
enabled = (state === true);
|
||||
|
||||
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
|
||||
}
|
||||
|
||||
phantomas.log('Overwritting phantomas spy function');
|
||||
|
||||
function spy(obj, fn, callbackBefore, callbackAfter) {
|
||||
var origFn = obj[fn];
|
||||
|
||||
if (typeof origFn !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
phantomas.log('Attaching a spy to "' + fn + '" function...');
|
||||
|
||||
obj[fn] = function() {
|
||||
var result;
|
||||
|
||||
// Before
|
||||
if (enabled) {
|
||||
callbackBefore.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Execute
|
||||
try {
|
||||
result = origFn.apply(this, arguments);
|
||||
} catch(e) {
|
||||
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
|
||||
phantomas.log(arguments);
|
||||
} finally {
|
||||
|
||||
// After
|
||||
if (enabled && callbackAfter) {
|
||||
callbackAfter.call(this, result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// copy custom properties of original function to the mocked one
|
||||
Object.keys(origFn).forEach(function(key) {
|
||||
obj[fn][key] = origFn[key];
|
||||
});
|
||||
|
||||
obj[fn].prototype = origFn.prototype;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
phantomas.spyEnabled = spyEnabled;
|
||||
phantomas.spy = spy;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
// Adding some code for the Javascript execution tree construction
|
||||
(function() {
|
||||
|
||||
var root = new ContextTreeNode(null, {type: 'main'});
|
||||
var currentContext = root;
|
||||
if (deepAnalysis) {
|
||||
phantomas.log('Entering deep Javascript analysis mode');
|
||||
}
|
||||
var depth = 0;
|
||||
|
||||
// Add a child but don't enter his context
|
||||
function pushContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a child to the current context and enter his context
|
||||
function enterContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext = currentContext.addChild(data);
|
||||
}
|
||||
depth ++;
|
||||
}
|
||||
|
||||
// Save given data in the current context and jump change current context to its parent
|
||||
function leaveContext(moreData) {
|
||||
if (depth === 1 || deepAnalysis) {
|
||||
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
}
|
||||
}
|
||||
depth --;
|
||||
}
|
||||
|
||||
function getContextData() {
|
||||
return currentContext.data;
|
||||
}
|
||||
|
||||
// Returns a clean object, without the parent which causes recursive loops
|
||||
function readFullTree() {
|
||||
// Return null if the contextTree is not correctly closed
|
||||
if (root !== currentContext) {
|
||||
return null;
|
||||
}
|
||||
var current = currentContext;
|
||||
|
||||
function recusiveRead(node) {
|
||||
if (node.children.length === 0) {
|
||||
delete node.children;
|
||||
} else {
|
||||
for (var i=0, max=node.children.length ; i<max ; i++) {
|
||||
recusiveRead(node.children[i]);
|
||||
}
|
||||
}
|
||||
delete node.parent;
|
||||
}
|
||||
recusiveRead(root);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
function ContextTreeNode(parent, data) {
|
||||
this.data = data;
|
||||
this.parent = parent;
|
||||
this.children = [];
|
||||
|
||||
this.addChild = function(data) {
|
||||
var child = new ContextTreeNode(this, data);
|
||||
this.children.push(child);
|
||||
return child;
|
||||
};
|
||||
}
|
||||
|
||||
phantomas.log('Adding some contextTree functions to phantomas');
|
||||
phantomas.pushContext = pushContext;
|
||||
phantomas.enterContext = enterContext;
|
||||
phantomas.leaveContext = leaveContext;
|
||||
phantomas.getContextData = getContextData;
|
||||
phantomas.readFullTree = readFullTree;
|
||||
|
||||
})();
|
||||
|
||||
})(window.__phantomas);
|
||||
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
|
||||
});
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Analyzes DOM complexity
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '1.0.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
// total length of HTML comments (including <!-- --> brackets)
|
||||
phantomas.setMetric('commentsSize'); // @desc the size of HTML comments on the page @offenders
|
||||
|
||||
// total length of text nodes with whitespaces only (i.e. pretty formatting of HTML)
|
||||
phantomas.setMetric('whiteSpacesSize'); // @desc the size of text nodes with whitespaces only
|
||||
|
||||
// count all tags
|
||||
phantomas.setMetric('DOMelementsCount'); // @desc total number of HTML element nodes
|
||||
phantomas.setMetric('DOMelementMaxDepth'); // @desc maximum level on nesting of HTML element node
|
||||
|
||||
// nodes with inlines CSS (style attribute)
|
||||
phantomas.setMetric('nodesWithInlineCSS'); // @desc number of nodes with inline CSS styling (with style attribute) @offenders
|
||||
|
||||
// images
|
||||
phantomas.setMetric('imagesScaledDown'); // @desc number of <img> nodes that have images scaled down in HTML @offenders
|
||||
phantomas.setMetric('imagesWithoutDimensions'); // @desc number of <img> nodes without both width and height attribute @offenders
|
||||
|
||||
// duplicated ID (issue #392)
|
||||
phantomas.setMetric('DOMidDuplicated'); // @desc number of duplicated IDs found in DOM
|
||||
|
||||
var Collection = require('../../../node_modules/phantomas/lib/collection'),
|
||||
DOMids = new Collection();
|
||||
|
||||
phantomas.on('domId', function(id) {
|
||||
DOMids.push(id);
|
||||
});
|
||||
|
||||
// HTML size
|
||||
phantomas.on('report', function() {
|
||||
phantomas.setMetricEvaluate('bodyHTMLSize', function() { // @desc the size of body tag content (document.body.innerHTML.length)
|
||||
return document.body && document.body.innerHTML.length || 0;
|
||||
});
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
var runner = new phantomas.nodeRunner(),
|
||||
whitespacesRegExp = /^\s+$/,
|
||||
DOMelementMaxDepth = 0,
|
||||
DOMelementMaxDepthElts = [],
|
||||
size = 0;
|
||||
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
case Node.COMMENT_NODE:
|
||||
size = node.textContent.length + 7; // '<!--' + '-->'.length
|
||||
phantomas.incrMetric('commentsSize', size);
|
||||
|
||||
// log HTML comments bigger than 64 characters
|
||||
if (size > 64) {
|
||||
phantomas.addOffender('commentsSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
|
||||
}
|
||||
break;
|
||||
|
||||
case Node.ELEMENT_NODE:
|
||||
phantomas.incrMetric('DOMelementsCount');
|
||||
|
||||
if (depth > DOMelementMaxDepth) {
|
||||
DOMelementMaxDepth = depth;
|
||||
DOMelementMaxDepthElts = [phantomas.getDOMPath(node)];
|
||||
} else if (depth === DOMelementMaxDepth) {
|
||||
DOMelementMaxDepthElts.push(phantomas.getDOMPath(node));
|
||||
}
|
||||
|
||||
// report duplicated ID (issue #392)
|
||||
if (node.id) {
|
||||
phantomas.emit('domId', node.id);
|
||||
}
|
||||
|
||||
// ignore inline <script> tags
|
||||
if (node.nodeName === 'SCRIPT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// images
|
||||
if (node.nodeName === 'IMG') {
|
||||
if (!node.hasAttribute('width') || !node.hasAttribute('height')) {
|
||||
phantomas.incrMetric('imagesWithoutDimensions');
|
||||
phantomas.addOffender('imagesWithoutDimensions', '%s <%s>', phantomas.getDOMPath(node), node.src);
|
||||
}
|
||||
if (node.naturalHeight && node.naturalWidth && node.height && node.width) {
|
||||
if (node.naturalHeight > node.height || node.naturalWidth > node.width) {
|
||||
phantomas.incrMetric('imagesScaledDown');
|
||||
phantomas.addOffender('imagesScaledDown', '%s (%dx%d -> %dx%d)', node.src, node.naturalWidth, node.naturalHeight, node.width, node.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// count nodes with inline CSS
|
||||
if (node.hasAttribute('style')) {
|
||||
phantomas.incrMetric('nodesWithInlineCSS');
|
||||
phantomas.addOffender('nodesWithInlineCSS', phantomas.getDOMPath(node) + ' (' + node.getAttribute('style') + ')');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Node.TEXT_NODE:
|
||||
if (whitespacesRegExp.test(node.textContent)) {
|
||||
phantomas.incrMetric('whiteSpacesSize', node.textContent.length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.setMetric('DOMelementMaxDepth', DOMelementMaxDepth);
|
||||
DOMelementMaxDepthElts.forEach(function(path) {
|
||||
phantomas.addOffender('DOMelementMaxDepth', path);
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(false, 'counting iframes and images');
|
||||
|
||||
// count <iframe> tags
|
||||
phantomas.setMetric('iframesCount', document.querySelectorAll('iframe').length); // @desc number of iframe nodes
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
}(window.__phantomas));
|
||||
});
|
||||
|
||||
DOMids.sort().forEach(function(id, cnt) {
|
||||
if (cnt > 1) {
|
||||
phantomas.incrMetric('DOMidDuplicated');
|
||||
phantomas.addOffender('DOMidDuplicated', '%s: %d occurrences', id, cnt);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* Analyzes DOM queries done via native DOM methods
|
||||
*/
|
||||
/* global Element: true, Document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '0.9.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
|
||||
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
|
||||
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
|
||||
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
|
||||
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
|
||||
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
|
||||
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
|
||||
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
|
||||
|
||||
// fake native DOM functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function querySpy(type, query, fnName, context) {
|
||||
phantomas.emit('domQuery', type, query, fnName, context); // @desc DOM query has been made
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementById', function(id) {
|
||||
phantomas.incrMetric('DOMqueriesById');
|
||||
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
|
||||
querySpy('id', '#' + id, 'getElementById', '#document');
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementById',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: '#document'
|
||||
},
|
||||
arguments: ['#' + id]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
var moreData = {
|
||||
resultsNumber : result ? 1 : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
});
|
||||
|
||||
// selectors by class name
|
||||
function selectorClassNameSpyBefore(className) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.incrMetric('DOMqueriesByClassName');
|
||||
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
|
||||
querySpy('class', '.' + className, 'getElementsByClassName', context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementsByClassName',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: ['.' + className]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorClassNameAfter(result) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
|
||||
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
|
||||
|
||||
// selectors by tag name
|
||||
function selectorTagNameSpyBefore(tagName) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.incrMetric('DOMqueriesByTagName');
|
||||
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
|
||||
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementsByTagName',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: [tagName]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorTagNameSpyAfter(result) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
|
||||
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
|
||||
|
||||
// selector queries
|
||||
function selectorQuerySpy(selector, context) {
|
||||
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
|
||||
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
|
||||
querySpy('selector', selector, 'querySelectorAll', context);
|
||||
}
|
||||
|
||||
function selectorQuerySpyBefore(selector) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
selectorQuerySpy(selector, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'querySelector',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorQuerySpyAfter(result) {
|
||||
var moreData = {
|
||||
resultsNumber : result ? 1 : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
function selectorAllQuerySpyBefore(selector) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
selectorQuerySpy(selector, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'querySelectorAll',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorAllQuerySpryAfter(result) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
|
||||
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
|
||||
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
|
||||
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
|
||||
|
||||
|
||||
// count DOM inserts
|
||||
function appendChild(child, element, context, appended) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
// ignore appending to the node that's not yet added to DOM tree
|
||||
if (!element.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't count elements added to fragments as a DOM inserts (issue #350)
|
||||
// DocumentFragment > div[0]
|
||||
if (context.indexOf('DocumentFragment') === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
phantomas.incrMetric('DOMinserts');
|
||||
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
|
||||
|
||||
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
|
||||
}
|
||||
|
||||
function appendChildSpyBefore(child) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
var appended = phantomas.getDOMPath(child);
|
||||
appendChild(child, this, context, appended);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'appendChild',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: [appended]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function insertBeforeSpyBefore(child) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
var appended = phantomas.getDOMPath(child);
|
||||
appendChild(child, this, context, appended);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'insertBefore',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
},
|
||||
arguments: [appended]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
|
||||
// count DOM queries by either ID, tag name, class name and selector query
|
||||
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
|
||||
var Collection = require('../../../node_modules/phantomas/lib/collection'),
|
||||
DOMqueries = new Collection();
|
||||
|
||||
phantomas.on('domQuery', function(type, query, fnName, context) {
|
||||
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
|
||||
phantomas.incrMetric('DOMqueries');
|
||||
|
||||
if (context && context.indexOf('DocumentFragment') === -1) {
|
||||
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
DOMqueries.sort().forEach(function(query, cnt) {
|
||||
if (cnt > 1) {
|
||||
phantomas.incrMetric('DOMqueriesDuplicated');
|
||||
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
|
||||
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Analyzes events bound to DOM elements
|
||||
*/
|
||||
/* global Document: true, Element: true, window: true */
|
||||
|
||||
exports.version = '0.2.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
|
||||
|
||||
// spy calls to EventTarget.addEventListener
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function eventSpyBefore(eventType) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
|
||||
|
||||
phantomas.incrMetric('eventsBound');
|
||||
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'addEventListener',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: path
|
||||
},
|
||||
arguments: [eventType]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function eventSpyAfter(result) {
|
||||
phantomas.leaveContext();
|
||||
}
|
||||
|
||||
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,326 +0,0 @@
|
||||
/**
|
||||
* Analyzes jQuery activity
|
||||
*
|
||||
* @see http://code.jquery.com/jquery-1.10.2.js
|
||||
* @see http://code.jquery.com/jquery-2.0.3.js
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
/* jshint -W030 */
|
||||
|
||||
exports.version = '0.2.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
|
||||
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
|
||||
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
|
||||
phantomas.setMetric('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request
|
||||
phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes)
|
||||
|
||||
var jQueryFunctions = [
|
||||
// DOM manipulations
|
||||
'html',
|
||||
'append',
|
||||
'appendTo',
|
||||
'prepend',
|
||||
'prependTo',
|
||||
'before',
|
||||
'insertBefore',
|
||||
'after',
|
||||
'insertAfter',
|
||||
'remove',
|
||||
'detach',
|
||||
'empty',
|
||||
'clone',
|
||||
'replaceWith',
|
||||
'replaceAll',
|
||||
'text',
|
||||
'wrap',
|
||||
'wrapAll',
|
||||
'wrapInner',
|
||||
'unwrap',
|
||||
|
||||
// Style manipulations
|
||||
'css',
|
||||
'offset',
|
||||
'position',
|
||||
'height',
|
||||
'innerHeight',
|
||||
'outerHeight',
|
||||
'width',
|
||||
'innerWidth',
|
||||
'outerWidth',
|
||||
'scrollLeft',
|
||||
'scrollTop',
|
||||
|
||||
// generic events
|
||||
'on',
|
||||
'off',
|
||||
'live',
|
||||
'die',
|
||||
'delegate',
|
||||
'undelegate',
|
||||
'one',
|
||||
'unbind',
|
||||
|
||||
// more events
|
||||
'blur',
|
||||
'change',
|
||||
'click',
|
||||
'dblclick',
|
||||
'error',
|
||||
'focus',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'hover',
|
||||
'keydown',
|
||||
'keypress',
|
||||
'keyup',
|
||||
'load',
|
||||
'mousedown',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'mousemove',
|
||||
'mouseout',
|
||||
'mouseover',
|
||||
'mouseup',
|
||||
'resize',
|
||||
'scroll',
|
||||
'select',
|
||||
'submit',
|
||||
'toggle',
|
||||
'unload',
|
||||
|
||||
// attributes
|
||||
'attr',
|
||||
'prop',
|
||||
'removeAttr',
|
||||
'removeProp',
|
||||
'val',
|
||||
'hasClass',
|
||||
'addClass',
|
||||
'removeClass',
|
||||
'toggleClass'
|
||||
];
|
||||
|
||||
// spy calls to jQuery functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function(jQueryFunctions) {
|
||||
(function(phantomas) {
|
||||
var jQuery;
|
||||
|
||||
// TODO: create a helper - phantomas.spyGlobalVar() ?
|
||||
window.__defineSetter__('jQuery', function(val) {
|
||||
var version;
|
||||
var jQueryFn;
|
||||
var oldJQuery = jQuery;
|
||||
|
||||
if (!val || !val.fn) {
|
||||
phantomas.log('jQuery: unable to detect version!');
|
||||
return;
|
||||
}
|
||||
|
||||
version = val.fn.jquery;
|
||||
jQuery = val;
|
||||
jQueryFn = val.fn;
|
||||
// Older jQuery (v?.?) compatibility
|
||||
if (!jQueryFn) {
|
||||
jQueryFn = jQuery;
|
||||
}
|
||||
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
phantomas.emit('jQueryLoaded', version);
|
||||
|
||||
phantomas.pushContext({
|
||||
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
|
||||
callDetails: {
|
||||
arguments: ['version ' + version]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
// jQuery.ready.promise
|
||||
// works for jQuery 1.8.0+ (released Aug 09 2012)
|
||||
phantomas.spy(val.ready, 'promise', function(func) {
|
||||
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'jQuery - onDOMReady',
|
||||
callDetails: {
|
||||
arguments: [func]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
|
||||
|
||||
|
||||
// Sizzle calls - jQuery.find
|
||||
// works for jQuery 1.3+ (released Jan 13 2009)
|
||||
phantomas.spy(val, 'find', function(selector, context) {
|
||||
phantomas.incrMetric('jQuerySizzleCalls');
|
||||
phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')');
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - find',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: this.length,
|
||||
firstElementPath: phantomas.getDOMPath(context)
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
|
||||
|
||||
|
||||
// $().bind - jQuery.bind
|
||||
// works for jQuery v?.?
|
||||
phantomas.spy(jQueryFn, 'bind', function(eventTypes, func) {
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - bind',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: this.length,
|
||||
firstElementPath: phantomas.getDOMPath(this[0]),
|
||||
selector: this.selector
|
||||
},
|
||||
arguments: [eventTypes, func]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
phantomas.leaveContext();
|
||||
}) || phantomas.log('jQuery: can not measure jQueryBindCalls (jQuery used on the page is too old)!');
|
||||
|
||||
|
||||
|
||||
// Add spys on many jQuery functions
|
||||
jQueryFunctions.forEach(function(functionName) {
|
||||
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
|
||||
|
||||
phantomas.spy(jQueryFn, functionName, function(args) {
|
||||
|
||||
// Clean args
|
||||
args = [].slice.call(arguments);
|
||||
args.forEach(function(arg, index) {
|
||||
|
||||
if (arg instanceof Object) {
|
||||
|
||||
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
|
||||
|
||||
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
|
||||
|
||||
} else if (arg instanceof HTMLElement) {
|
||||
|
||||
arg = phantomas.getDOMPath(arg) || 'unknown';
|
||||
|
||||
} else if (typeof arg === 'function') {
|
||||
|
||||
arg = '(function)';
|
||||
|
||||
} else {
|
||||
|
||||
try {
|
||||
arg = JSON.stringify(arg);
|
||||
} catch(e) {
|
||||
arg = '[Object]';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
|
||||
arg = arg.substring(0, 200) + '...';
|
||||
}
|
||||
|
||||
if (typeof arg === 'function') {
|
||||
arg = '(function)';
|
||||
}
|
||||
|
||||
if (arg === true) {
|
||||
arg = 'true';
|
||||
}
|
||||
|
||||
if (arg === false) {
|
||||
arg = 'false';
|
||||
}
|
||||
|
||||
if (arg === null) {
|
||||
arg = 'null';
|
||||
}
|
||||
|
||||
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
|
||||
arg = 'undefined';
|
||||
}
|
||||
|
||||
args[index] = arg;
|
||||
});
|
||||
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - ' + functionName,
|
||||
callDetails: {
|
||||
context: {
|
||||
length: this.length,
|
||||
firstElementPath: phantomas.getDOMPath(this[0])
|
||||
},
|
||||
arguments: args
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
phantomas.leaveContext();
|
||||
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
window.__defineGetter__('jQuery', function() {
|
||||
return jQuery;
|
||||
});
|
||||
})(window.__phantomas);
|
||||
}, jQueryFunctions);
|
||||
});
|
||||
|
||||
|
||||
// count Sizzle calls to detect duplicated queries
|
||||
var Collection = require('../../../node_modules/phantomas/lib/collection'),
|
||||
sizzleCalls = new Collection(),
|
||||
jQueryLoading = new Collection();
|
||||
|
||||
phantomas.on('onSizzleCall', function(request) {
|
||||
sizzleCalls.push(request);
|
||||
});
|
||||
|
||||
phantomas.on('jQueryLoaded', function(version) {
|
||||
jQueryLoading.push(version);
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
sizzleCalls.sort().forEach(function(id, cnt) {
|
||||
if (cnt > 1) {
|
||||
phantomas.incrMetric('jQuerySizzleCallsDuplicated');
|
||||
phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt);
|
||||
}
|
||||
});
|
||||
|
||||
jQueryLoading.forEach(function(version) {
|
||||
phantomas.incrMetric('jQueryDifferentVersions');
|
||||
phantomas.addOffender('jQueryDifferentVersions', '%s', version);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
|
||||
*/
|
||||
|
||||
exports.version = '0.3.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
|
||||
|
||||
function formatTrace(trace) {
|
||||
var ret = [];
|
||||
|
||||
if(Array.isArray(trace)) {
|
||||
trace.forEach(function(entry) {
|
||||
ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
phantomas.on('jserror', function(msg, trace) {
|
||||
trace = formatTrace(trace);
|
||||
|
||||
phantomas.log(msg);
|
||||
phantomas.log('Backtrace: ' + trace.join(' / '));
|
||||
|
||||
phantomas.incrMetric('jsErrors');
|
||||
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
|
||||
|
||||
// Yeah, this is weird, i'm sending the error back to the browser...
|
||||
phantomas.evaluate(function(msg, caller, trace) {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'error',
|
||||
callDetails: {
|
||||
arguments: [msg]
|
||||
},
|
||||
caller: caller,
|
||||
backtrace: trace
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
}, msg, trace[0], trace.join(' / '));
|
||||
});
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
|
||||
*/
|
||||
|
||||
exports.version = '0.0';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
/*phantomas.on('recv', function(entry, res) {
|
||||
if (!entry.isJS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Yeah, this is weird, i'm sending the information back to the browser...
|
||||
phantomas.evaluate(function(url) {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'script loaded',
|
||||
callDetails: {
|
||||
arguments: [url]
|
||||
}
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
}, entry.url);
|
||||
});*/
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Saves the javascript interractions with the DOM
|
||||
*
|
||||
* Run phantomas with --js-execution-tree option to use this module
|
||||
*/
|
||||
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
|
||||
|
||||
// save data
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log('Reading execution tree JSON');
|
||||
|
||||
phantomas.evaluate(function() {(function(phantomas) {
|
||||
var fullTree = phantomas.readFullTree();
|
||||
|
||||
if (fullTree === null) {
|
||||
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
|
||||
return;
|
||||
}
|
||||
|
||||
phantomas.setMetric('javascriptExecutionTree', true, true);
|
||||
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
|
||||
|
||||
})(window.__phantomas);});
|
||||
});
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Analyzes if HTTP responses keep the connections alive.
|
||||
*/
|
||||
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('closedConnections'); // @desc requests not keeping the connection alive and slowing down the next request @offenders
|
||||
|
||||
var closedConnectionHosts = {};
|
||||
|
||||
phantomas.on('recv', function(entry, res) {
|
||||
var connectionHeader = (entry.headers.Connection || '').toLowerCase();
|
||||
// Taking the protocol in account, in case the same domain is called with two different protocols.
|
||||
var host = entry.protocol + '://' + entry.domain;
|
||||
|
||||
if (connectionHeader.indexOf('close') >= 0) {
|
||||
// Don't blame it immediatly, wait to see if the connection is needed a second time.
|
||||
closedConnectionHosts[host] = entry.url;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('send', function(entry, res) {
|
||||
var host = entry.protocol + '://' + entry.domain;
|
||||
var previousClosedConnection = closedConnectionHosts[host];
|
||||
|
||||
if (previousClosedConnection) {
|
||||
// There was a closed connection. We can blame it safely now!
|
||||
phantomas.incrMetric('closedConnections');
|
||||
phantomas.addOffender('closedConnections', previousClosedConnection);
|
||||
|
||||
closedConnectionHosts[host] = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* Measure when the page reaches certain states
|
||||
*
|
||||
* @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
|
||||
* @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '1.0.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
// times below are calculated relative to performance.timing.responseEnd (#117)
|
||||
phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM
|
||||
phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
|
||||
phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
|
||||
phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning
|
||||
|
||||
// backend vs frontend time
|
||||
phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%]
|
||||
phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
|
||||
|
||||
// measure dom... metrics from the moment HTML response was fully received
|
||||
var responseEndTime = Date.now();
|
||||
|
||||
phantomas.on('responseEnd', function() {
|
||||
responseEndTime = Date.now();
|
||||
phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
|
||||
});
|
||||
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime) {
|
||||
(function(phantomas) {
|
||||
phantomas.spyEnabled(false, 'installing window.performance metrics');
|
||||
|
||||
phantomas.currentStep = 'domCreation';
|
||||
|
||||
// extend window.performance
|
||||
// "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
|
||||
if (typeof window.performance === 'undefined') {
|
||||
window.performance = {
|
||||
timing: {
|
||||
responseEnd: responseEndTime
|
||||
}
|
||||
};
|
||||
|
||||
phantomas.log('Performance timing: emulating window.performance');
|
||||
}
|
||||
else {
|
||||
phantomas.log('Performance timing: using native window.performance');
|
||||
}
|
||||
|
||||
// onDOMReady
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
setTimeout(function() {
|
||||
// use NavigationTiming if possible
|
||||
var time = window.performance.timing.domContentLoadedEventEnd ?
|
||||
(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
|
||||
:
|
||||
(Date.now() - responseEndTime);
|
||||
|
||||
phantomas.currentStep = 'domContentLoadedEnd';
|
||||
phantomas.setMetric('domContentLoadedEnd', time, true);
|
||||
phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'domContentLoadedEnd'
|
||||
});
|
||||
}, 0);
|
||||
|
||||
var time = Date.now() - responseEndTime;
|
||||
|
||||
phantomas.currentStep = 'domContentLoaded';
|
||||
phantomas.setMetric('domContentLoaded', time, true);
|
||||
phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'domContentLoaded'
|
||||
});
|
||||
});
|
||||
|
||||
// emulate Navigation Timing
|
||||
document.addEventListener('readystatechange', function() {
|
||||
var readyState = document.readyState,
|
||||
responseEndTime = window.performance.timing.responseEnd,
|
||||
time = Date.now() - responseEndTime,
|
||||
metricName;
|
||||
|
||||
// @see http://www.w3.org/TR/html5/dom.html#documentreadystate
|
||||
switch(readyState) {
|
||||
// the browser has finished parsing all of the HTML and DOM construction is complete
|
||||
case 'interactive':
|
||||
metricName = 'domInteractive';
|
||||
break;
|
||||
|
||||
// the processing is complete and all of the resources on the page have finished downloading
|
||||
case 'complete':
|
||||
metricName = 'domComplete';
|
||||
phantomas.log('Performance timing: %j', window.performance.timing);
|
||||
break;
|
||||
|
||||
default:
|
||||
phantomas.log('Performance timing: unhandled "%s" state!', readyState);
|
||||
return;
|
||||
}
|
||||
|
||||
phantomas.currentStep = metricName;
|
||||
phantomas.setMetric(metricName, time, true);
|
||||
phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
|
||||
|
||||
phantomas.pushContext({
|
||||
type: metricName
|
||||
});
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
})(window.__phantomas);
|
||||
}, responseEndTime);
|
||||
});
|
||||
|
||||
/**
|
||||
* Emit metrics with backend vs frontend time
|
||||
*
|
||||
* Performance Golden Rule:
|
||||
* "80-90% of the end-user response time is spent on the frontend. Start there."
|
||||
*
|
||||
* @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
|
||||
*/
|
||||
phantomas.on('report', function() {
|
||||
// The “backend” time is the time it takes the server to get the first byte back to the client.
|
||||
// The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
|
||||
var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
|
||||
frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
|
||||
totalTime = backendTime + frontendTime,
|
||||
backendTimePercentage;
|
||||
|
||||
if (totalTime === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
backendTimePercentage = Math.round(backendTime / totalTime * 100);
|
||||
|
||||
phantomas.setMetric('timeBackend', backendTimePercentage);
|
||||
phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
|
||||
|
||||
phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
|
||||
});
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
// Config file
|
||||
var settings = require('./server_config/settings.json');
|
||||
|
||||
// Libraries
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var io = require('socket.io').listen(server);
|
||||
var bodyParser = require('body-parser');
|
||||
var compress = require('compression');
|
||||
|
||||
// Internals
|
||||
var indexController = require('./app/node_controllers/indexController');
|
||||
var launchTestController = require('./app/node_controllers/launchTestController');
|
||||
var resultsController = require('./app/node_controllers/resultsController');
|
||||
var waitingQueueSocket = require('./app/node_controllers/waitingQueueSocket');
|
||||
var testQueue = require('./app/lib/testQueue');
|
||||
|
||||
app.use(compress());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
|
||||
|
||||
// Redirect www.yellowlab.tools to yellowlab.tools (for SEO)
|
||||
app.all('*', function(req, res, next) {
|
||||
if (req.hostname && req.hostname.match(/^www\.yellowlab\.tools/) !== null) {
|
||||
res.redirect('http://' + req.hostname.replace(/^www\.yellowlab\.tools/, 'yellowlab.tools') + req.url);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Routes definition
|
||||
app.get('/', function(req, res) { indexController(req, res, settings.googleAnalyticsId); });
|
||||
app.post('/launchTest', function(req, res) { launchTestController(req, res, testQueue, settings.googleAnalyticsId); });
|
||||
app.get('/results/:testId', function(req, res) { resultsController(req, res, settings.googleAnalyticsId); });
|
||||
|
||||
|
||||
// Static files
|
||||
app.use('/public', express.static(__dirname + '/app/public'));
|
||||
app.use('/bower_components', express.static(__dirname + '/bower_components'));
|
||||
|
||||
|
||||
// Socket.io
|
||||
io.on('connection', function(socket){
|
||||
waitingQueueSocket(socket, testQueue);
|
||||
});
|
||||
|
||||
// Create the results folder if it doesn't exist
|
||||
var resultsPath = 'results';
|
||||
if (!fs.existsSync(resultsPath)) {
|
||||
fs.mkdirSync(resultsPath);
|
||||
}
|
||||
|
||||
// Launch the server
|
||||
server.listen(settings.serverPort, function() {
|
||||
console.log('Listening on port %d', server.address().port);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# APT-GET
|
||||
sudo apt-get update
|
||||
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
|
||||
sudo apt-get install curl git python-software-properties -y --force-yes
|
||||
|
||||
# Installation of NodeJS
|
||||
sudo add-apt-repository -y ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nodejs
|
||||
source ~/.profile
|
||||
|
||||
# Installation of some packages globally
|
||||
npm install bower -g
|
||||
npm install forever -g
|
||||
|
||||
# Installation of YellowLabTools
|
||||
sudo mkdir /space
|
||||
sudo chown $USER /space
|
||||
cd /space
|
||||
git clone https://github.com/gmetais/YellowLabTools.git --branch master
|
||||
cd YellowLabTools
|
||||
npm install --production
|
||||
bower install --config.interactive=false --allow-root
|
||||
|
||||
# Start the server
|
||||
rm server_config/settings.json
|
||||
cp server_config/settings-prod.json server_config/settings.json
|
||||
forever start -c "node --stack-size=65500" server.js
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd /space/YellowLabTools
|
||||
|
||||
# Stop the server
|
||||
forever stopall
|
||||
|
||||
# Keep the settings.json file
|
||||
git stash
|
||||
git pull
|
||||
git stash pop
|
||||
|
||||
# In case something was added in package.json or bower.json
|
||||
rm -rf node_modules
|
||||
npm install --production
|
||||
rm -rf bower_components
|
||||
bower install --config.interactive=false --allow-root
|
||||
|
||||
# Restart the server
|
||||
forever start -c "node --stack-size=65500" server.js
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"serverPort": 80,
|
||||
"googleAnalyticsId": "UA-54493828-1"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"serverPort": 8383,
|
||||
"googleAnalyticsId": ""
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
var path = require('path');
|
||||
var srcDir = path.join(__dirname, '..', 'server');
|
||||
|
||||
require('blanket')({
|
||||
// Only files that match the pattern will be instrumented
|
||||
pattern: srcDir
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
var should = require('chai').should();
|
||||
var colorDiff = require('../../lib/tools/colorDiff');
|
||||
|
||||
describe('colorDiff', function() {
|
||||
|
||||
it('should parse offenders correctly', function() {
|
||||
colorDiff.parseOffender({url:'file.css', value: {message: '#000 (1 times)'}}).should.equal('#000');
|
||||
colorDiff.parseOffender({url:'file.css', value: {message: '#5bc0de (2 times)'}}).should.equal('#5bc0de');
|
||||
colorDiff.parseOffender({url:'file.css', value: {message: 'rgba(0,0,0,0.075) (100 times)'}}).should.equal('rgba(0,0,0,0.075)');
|
||||
colorDiff.parseOffender({url:'file.css', value: {message: 'rgb(91,192,222) (1000 times)'}}).should.equal('rgb(91,192,222)');
|
||||
});
|
||||
|
||||
it('should parse colors correctly', function() {
|
||||
colorDiff.parseColor('#000').should.deep.equal({R: 0, G: 0, B: 0, A: 1, Lab: {L: 0, a: 0, b: 0}, original: '#000'});
|
||||
colorDiff.parseColor('rgba(255,255,255,0.5)').should.deep.equal({R: 255, G: 255, B: 255, A: 0.5, Lab: {L: 100, a: 0.00526049995830391, b: -0.010408184525267927}, original: 'rgba(255,255,255,0.5)'});
|
||||
});
|
||||
|
||||
it('should not compare colors with different alpha', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 0, 0.5)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(false);
|
||||
});
|
||||
|
||||
it('should find that two colors are similar', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 1, 1)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(true);
|
||||
});
|
||||
|
||||
it('should find that two colors are similar even with alpha', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 0.3)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 2, 0.3)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(true);
|
||||
});
|
||||
|
||||
it('should find that two colors are different', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(99, 99, 99, 1)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(false);
|
||||
});
|
||||
|
||||
it('should compare all colors to each other', function() {
|
||||
var colors = [
|
||||
{url:'file.css', value: {message: '#000 (1 times)'}},
|
||||
{url:'file.css', value: {message: '#5bc0de (2 times)'}},
|
||||
{url:'file.css', value: {message: 'rgba(0,0,0,0.075) (100 times)'}},
|
||||
{url:'file.css', value: {message: 'rgb(91,192,222) (1000 times)'}},
|
||||
{url:'file.css', value: {message: 'rgba(0,0,1,1) (1 times)'}},
|
||||
{url:'file.css', value: {message: 'rgba(99,99,99,1) (1 times)'}},
|
||||
{url:'file.css', value: {message: 'rgba(100,100,100,1) (1 times)'}}
|
||||
];
|
||||
|
||||
var data = {
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
offenders: {
|
||||
cssColors: colors
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
newData = colorDiff.compareAllColors(data);
|
||||
|
||||
newData.toolsResults.should.have.a.property('colorDiff');
|
||||
newData.toolsResults.colorDiff.should.have.a.property('metrics');
|
||||
newData.toolsResults.colorDiff.metrics.should.have.a.property('similarColors').that.equals(3);
|
||||
newData.toolsResults.colorDiff.should.have.a.property('offenders');
|
||||
newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([
|
||||
{
|
||||
color1: '#000',
|
||||
color2: 'rgba(0,0,1,1)',
|
||||
isDark: true
|
||||
},
|
||||
{
|
||||
color1: '#5bc0de',
|
||||
color2: 'rgb(91,192,222)',
|
||||
isDark: false
|
||||
},
|
||||
{
|
||||
color1: 'rgba(99,99,99,1)',
|
||||
color2: 'rgba(100,100,100,1)',
|
||||
isDark: true
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
var should = require('chai').should();
|
||||
var contentTypeChecker = require('../../lib/tools/redownload/contentTypeChecker');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('contentTypeChecker', function() {
|
||||
|
||||
var jpgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var pngImageContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var svgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var cssFileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
|
||||
it('detect the right content type', function() {
|
||||
contentTypeChecker.findContentType(jpgImageContent).mimes.should.deep.equal(['image/jpeg']);
|
||||
contentTypeChecker.findContentType(pngImageContent).mimes.should.deep.equal(['image/png']);
|
||||
contentTypeChecker.findContentType(svgImageContent).mimes.should.deep.equal(['image/svg+xml']);
|
||||
should.equal(contentTypeChecker.findContentType(cssFileContent), null);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
var should = require('chai').should();
|
||||
var rulesChecker = require('../../lib/rulesChecker');
|
||||
|
||||
describe('customPolicies', function() {
|
||||
|
||||
var policies = require('../../lib/metadata/policies.js');
|
||||
var results;
|
||||
|
||||
|
||||
it('should transform DOMelementMaxDepth offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMelementMaxDepth": 3
|
||||
},
|
||||
"offenders": {
|
||||
"DOMelementMaxDepth": [
|
||||
"body > div#foo > span.bar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMelementMaxDepth');
|
||||
results.DOMelementMaxDepth.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 1,
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#foo": {
|
||||
"span.bar": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMidDuplicated offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMidDuplicated": 2
|
||||
},
|
||||
"offenders": {
|
||||
"DOMidDuplicated": [
|
||||
{
|
||||
"id": "colorswitch-30883-30865",
|
||||
"occurrences": 4
|
||||
}, {
|
||||
"id": "foo",
|
||||
"occurrences": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMidDuplicated');
|
||||
results.DOMidDuplicated.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"id": "colorswitch-30883-30865",
|
||||
"occurrences": 4
|
||||
},
|
||||
{
|
||||
"id": "foo",
|
||||
"occurrences": 1
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should grade correctly jQuery versions', function() {
|
||||
|
||||
var versions = {
|
||||
'1.7.0': 0,
|
||||
'1.10.1': 20,
|
||||
'1.10.3a': 20,
|
||||
'2.0.0-rc1': 20,
|
||||
'1.11.1': 30,
|
||||
'2.1.1-beta1': 30,
|
||||
'1.12.1': 40,
|
||||
'2.2.1': 40,
|
||||
'3.0.1': 50,
|
||||
'3.1.0': 70,
|
||||
'3.2.1': 90,
|
||||
'3.3.1': 100,
|
||||
'3.5.0': 100
|
||||
};
|
||||
|
||||
for (var version in versions) {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": version
|
||||
},
|
||||
"offenders": {}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.jQueryVersion.score.should.equal(versions[version]);
|
||||
}
|
||||
|
||||
|
||||
// Unknown jQuery version
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": "wooot"
|
||||
},
|
||||
"offenders": {}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.should.not.have.a.property('jQueryVersion');
|
||||
|
||||
|
||||
// If jQueryVersionsLoaded is 0
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": "1.6.0",
|
||||
"jQueryVersionsLoaded": 0
|
||||
},
|
||||
"offenders": {}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.should.not.have.a.property('jQueryVersion');
|
||||
results.should.have.a.property('jQueryVersionsLoaded');
|
||||
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(100);
|
||||
|
||||
|
||||
// If there are more than 1 jQuery version
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": "1.6.0",
|
||||
"jQueryVersionsLoaded": 2
|
||||
},
|
||||
"offenders": {}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.should.not.have.a.property('jQueryVersion');
|
||||
results.should.have.a.property('jQueryVersionsLoaded');
|
||||
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0);
|
||||
results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(false);
|
||||
});
|
||||
|
||||
|
||||
it('should transform cssParsingErrors offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"cssParsingErrors": 2
|
||||
},
|
||||
"offenders": {
|
||||
"cssParsingErrors": [
|
||||
{
|
||||
"url": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css",
|
||||
"value": {
|
||||
"message": "Error: CSS parsing failed: missing '}'",
|
||||
"position": {
|
||||
"start": {
|
||||
"line": 4,
|
||||
"column": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"column": 72
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css",
|
||||
"value": {
|
||||
"message": "Empty CSS was provided"
|
||||
}
|
||||
},
|
||||
"http://www.sudexpress.com/skin/frontend/sudexpress/default/css/another.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('cssParsingErrors');
|
||||
results.cssParsingErrors.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 3,
|
||||
"list": [
|
||||
{
|
||||
"error": "Error: CSS parsing failed: missing '}'",
|
||||
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css",
|
||||
"line": 4,
|
||||
"column": 1
|
||||
},
|
||||
{
|
||||
"error": "Empty CSS was provided",
|
||||
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css",
|
||||
"line": null,
|
||||
"column": null
|
||||
},
|
||||
{
|
||||
"error": "Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.",
|
||||
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/another.css",
|
||||
"line": null,
|
||||
"column": null
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Enough for the moment, to be complete...
|
||||
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
var should = require('chai').should();
|
||||
var fileMinifier = require('../../lib/tools/redownload/fileMinifier');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('fileMinifier', function() {
|
||||
|
||||
it('should minify a JS file with minifyJs', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyJs(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a JS file with minifyFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail minifying an already minified JS', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery1.8.3.js'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('optimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail minifying a JS with syntax errors', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('optimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a CSS file with clean-css', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyCss(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a CSS file with minifyFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isCSS: true,
|
||||
type: 'css',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify an HTML file with ', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyHtml(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should avoid minifying some JS files known as minified', function() {
|
||||
fileMinifier.isKnownAsMinified('https://platform.twitter.com/widgets.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://platform.twitter.com/widgets.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/sdk.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/sdk.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/all.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/all.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://apis.google.com/js/plusone.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.js').should.equal(false);
|
||||
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.js').should.equal(false);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://ssl.google-analytics.com/ga.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/ga.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://www.google-analytics.com/analytics.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/analytics.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('http://anydomain.com/anyurl').should.equal(false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
var should = require('chai').should();
|
||||
var fontAnalyzer = require('../../lib/tools/redownload/fontAnalyzer');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('fontAnalyzer', function() {
|
||||
|
||||
it('should extract metrics from a font', function(done) {
|
||||
this.timeout(10000);
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/SourceSansPro/SourceSansPro-Regular.woff'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/SourceSansPro/SourceSansPro-Regular.woff',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isWebFont: true,
|
||||
type: 'webfont',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fontAnalyzer.getMetricsFromFont(entry, 'ABCD')
|
||||
|
||||
.then(function(metrics) {
|
||||
|
||||
metrics.should.be.an('Object');
|
||||
metrics.should.have.a.property('name').that.equals('Source Sans Pro');
|
||||
metrics.should.have.a.property('numGlyphs').that.equals(1944);
|
||||
metrics.should.have.a.property('averageGlyphComplexity').that.equals(26.6);
|
||||
metrics.should.have.a.property('compressedWeight').that.equals(fileSize);
|
||||
|
||||
metrics.should.have.a.property('unicodeRanges').that.is.an('Object');
|
||||
metrics.unicodeRanges.should.have.a.property('Basic Latin');
|
||||
metrics.unicodeRanges['Basic Latin'].should.have.a.property('charset').that.is.a('String');
|
||||
metrics.unicodeRanges['Basic Latin'].charset.length.should.equal(95);
|
||||
metrics.unicodeRanges['Basic Latin'].name.should.equal('Basic Latin');
|
||||
metrics.unicodeRanges['Basic Latin'].rangeStart.should.equal(0x0020);
|
||||
metrics.unicodeRanges['Basic Latin'].rangeEnd.should.equal(0x007F);
|
||||
metrics.unicodeRanges['Basic Latin'].coverage.should.equal(95 / 96);
|
||||
metrics.unicodeRanges['Basic Latin'].numGlyphsInCommonWithPageContent.should.equal(4);
|
||||
|
||||
metrics.unicodeRanges.Cyrillic.numGlyphsInCommonWithPageContent.should.equal(0);
|
||||
|
||||
metrics.should.have.a.property('numGlyphsInCommonWithPageContent').that.equals(4);
|
||||
|
||||
should.equal(metrics.unicodeRanges.Others.coverage, undefined);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort glyphes by unicode ranges', function() {
|
||||
var ranges = fontAnalyzer.readUnicodeRanges([0x0041, 0x0042, 0x0043, 0x0044, 0x0416], '0123AMZ');
|
||||
|
||||
ranges.should.deep.equal({
|
||||
'Basic Latin': {
|
||||
name: 'Basic Latin',
|
||||
rangeStart: 32,
|
||||
rangeEnd: 127,
|
||||
charset: 'ABCD',
|
||||
coverage: 0.041666666666666664,
|
||||
numGlyphsInCommonWithPageContent: 1
|
||||
},
|
||||
'Cyrillic': {
|
||||
name: 'Cyrillic',
|
||||
rangeStart: 1024,
|
||||
rangeEnd: 1327,
|
||||
charset: 'Ж',
|
||||
coverage: 0.003289473684210526,
|
||||
numGlyphsInCommonWithPageContent: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform an array of char codes into a string', function() {
|
||||
var str = fontAnalyzer.getCharacterSetAsString([0x0041, 0x0042, 0x0043, 0x0044, 0x0416]);
|
||||
str.should.equal('ABCDЖ');
|
||||
});
|
||||
|
||||
it('should find the right unicode range for a char', function() {
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0x0020).should.deep.equal({
|
||||
name: 'Basic Latin',
|
||||
rangeStart: 0x0020,
|
||||
rangeEnd: 0x007F
|
||||
});
|
||||
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0x0021).name.should.equal('Basic Latin');
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0x007F).name.should.equal('Basic Latin');
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0x007E).name.should.equal('Basic Latin');
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0x0000).name.should.equal('Others');
|
||||
fontAnalyzer.getUnicodeRangeFromChar(0xFFFFFFFFF).name.should.equal('Others');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,583 @@
|
||||
var should = require('chai').should();
|
||||
var gzipCompressor = require('../../lib/tools/redownload/gzipCompressor');
|
||||
var fileMinifier = require('../../lib/tools/redownload/fileMinifier');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('gzipCompressor', function() {
|
||||
|
||||
var minifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/minified-script.js'));
|
||||
var notMinifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
var someTextFileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
|
||||
it('should gzip a JS file that was not gziped but was minified', function(done) {
|
||||
var fileContent = minifiedJSContent;
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/minified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JS file that was not gziped and not minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var minifiedContent = minifiedJSContent;
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
var minifiedSize = minifiedContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
bodyAfterOptimization: minifiedContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: false,
|
||||
optimized: minifiedSize
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.not.undefined;
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(newEntry.weightCheck.afterGzipCompression);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JS file that is gziped but not minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var minifiedContent = minifiedJSContent;
|
||||
var fileSize = 6436;
|
||||
var gzipedSize = 2646;
|
||||
var minifiedSize = 1954;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
bodyAfterOptimization: minifiedContent.toString('utf8'),
|
||||
totalWeight: gzipedSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: gzipedSize,
|
||||
isCompressed: true,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: false,
|
||||
optimized: minifiedSize
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.not.undefined;
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(gzipedSize);
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(minifiedSize);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a JS file that was gziped and minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var fileSize = 6436;
|
||||
var gzipedSize = 2646;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: gzipedSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: gzipedSize,
|
||||
isCompressed: true,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('minified');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterGzipCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a CSS file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
status: 200,
|
||||
isCSS: true,
|
||||
type: 'css',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an HTML file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery-page.html',
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an SVG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
isSVG: true,
|
||||
type: 'image',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an XML file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.xml',
|
||||
status: 200,
|
||||
isXML: true,
|
||||
type: 'xml',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JSON file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.json',
|
||||
status: 200,
|
||||
isJSON: true,
|
||||
type: 'json',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a TTF file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.ttf',
|
||||
status: 200,
|
||||
isWebFont: true,
|
||||
isTTF: true,
|
||||
type: 'webfont',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should gzip a favicon file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.ico',
|
||||
status: 200,
|
||||
isFavicon: true,
|
||||
type: 'favicon',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a JPEG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jpeg-image.jpg',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterGzipCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not gzip a PNG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/png-image.png',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterGzipCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a GIF file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake gif, don't tell anyone...
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/gif-image.gif',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/gif',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterGzipCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a WEBP file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake webp, don't tell anyone...
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/webp-image.webp',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/webp',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterGzipCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
var should = require('chai').should();
|
||||
var imageDimensions = require('../../lib/tools/redownload/imageDimensions');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('imageDimensions', function() {
|
||||
|
||||
it('should detect png image dimensions', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: 999,
|
||||
headersSize: 200,
|
||||
bodySize: 999,
|
||||
isCompressed: false,
|
||||
uncompressedSize: 999
|
||||
}
|
||||
};
|
||||
|
||||
imageDimensions.getDimensions(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.should.have.a.property('imageDimensions');
|
||||
newEntry.imageDimensions.should.have.a.property('width').that.equals(664);
|
||||
newEntry.imageDimensions.should.have.a.property('height').that.equals(314);
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect a jpg image dimensions', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: 999,
|
||||
headersSize: 200,
|
||||
bodySize: 999,
|
||||
isCompressed: false,
|
||||
uncompressedSize: 999
|
||||
}
|
||||
};
|
||||
|
||||
imageDimensions.getDimensions(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.should.have.a.property('imageDimensions');
|
||||
newEntry.imageDimensions.should.have.a.property('width').that.equals(285);
|
||||
newEntry.imageDimensions.should.have.a.property('height').that.equals(427);
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,323 @@
|
||||
var should = require('chai').should();
|
||||
var imageOptimizer = require('../../lib/tools/redownload/imageOptimizer');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('imageOptimizer', function() {
|
||||
|
||||
it('should optimize a JPEG image losslessly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressJpegLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a JPEG image lossly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressJpegLossly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best optimization for a jpeg', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('lossy').that.is.below(newEntry.weightCheck.lossless);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a PNG image losslessly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to optimize an already optimized PNG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/logo-large.png'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.equal(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to optimize a non-PNG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.equal(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a png', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize an SVG image losslessly', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressSvgLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize an SVG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.svg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
isSVG: true,
|
||||
type: 'image',
|
||||
contentType: 'image/svg+xml',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.equals(newEntry.weightCheck.lossless);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('shouldn\'t fail optimizing a corrupted jpeg', function(done) {
|
||||
|
||||
// In this test, we try to optimize a PNG but with a falsy "image/jpeg" content type
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t fail optimizing a corrupted png', function(done) {
|
||||
|
||||
// In this test, we try to optimize a JPEG but with a falsy "image/png" content type
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should determine if gain is enough', function() {
|
||||
imageOptimizer.gainIsEnough(20000, 10000).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(2000, 1000).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(20000, 21000).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(20000, 40000).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(20000, 19500).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(250, 120).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(200, 120).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(2000, 1900).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(200000, 197000).should.equal(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
var chai = require('chai');
|
||||
var sinon = require('sinon');
|
||||
var sinonChai = require('sinon-chai');
|
||||
var should = chai.should();
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var ylt = require('../../lib/index');
|
||||
|
||||
chai.use(sinonChai);
|
||||
|
||||
|
||||
describe('index.js', function() {
|
||||
|
||||
it('should return a promise', function() {
|
||||
/*var promise = ylt();
|
||||
|
||||
promise.should.have.property('then').that.is.a('function');
|
||||
promise.should.have.property('fail').that.is.a('function');*/
|
||||
});
|
||||
|
||||
it('should fail with an undefined url', function(done) {
|
||||
ylt().fail(function(err) {
|
||||
err.should.be.a('string').that.equals('URL missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with an empty url string', function(done) {
|
||||
ylt('').fail(function(err) {
|
||||
err.should.be.a('string').that.equals('URL missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed on simple-page.html', function(done) {
|
||||
this.timeout(15000);
|
||||
|
||||
// Check if console.log is called
|
||||
sinon.spy(console, 'log');
|
||||
|
||||
var url = 'http://localhost:8388/simple-page.html';
|
||||
|
||||
ylt(url)
|
||||
.then(function(data) {
|
||||
|
||||
data.should.be.an('object');
|
||||
data.toolsResults.should.be.an('object');
|
||||
|
||||
// Test Phantomas
|
||||
data.toolsResults.phantomas.should.be.an('object');
|
||||
data.toolsResults.phantomas.should.have.a.property('url').that.equals(url);
|
||||
data.toolsResults.phantomas.should.have.a.property('metrics').that.is.an('object');
|
||||
data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(2);
|
||||
data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object');
|
||||
data.toolsResults.phantomas.offenders.should.have.a.property('DOMelementMaxDepth');
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(2);
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[0].should.equal('body > h1[0]');
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[1].should.equal('body > script[1]');
|
||||
|
||||
// Test rules
|
||||
data.should.have.a.property('rules').that.is.an('object');
|
||||
|
||||
data.rules.should.have.a.property('DOMelementMaxDepth').that.is.an('object');
|
||||
data.rules.DOMelementMaxDepth.should.deep.equal({
|
||||
policy: {
|
||||
"tool": "phantomas",
|
||||
"label": "DOM max depth",
|
||||
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
|
||||
"isOkThreshold": 15,
|
||||
"isBadThreshold": 25,
|
||||
"isAbnormalThreshold": 32,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"value": 1,
|
||||
"bad": false,
|
||||
"globalScoreIfFixed": 98,
|
||||
"abnormal": false,
|
||||
"score": 100,
|
||||
"abnormalityScore": 0,
|
||||
"offendersObj": {
|
||||
"count": 2,
|
||||
"tree": {
|
||||
"body": {
|
||||
"h1[0]": 1,
|
||||
"script[1]": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*jshint expr: true*/
|
||||
console.log.should.not.have.been.called;
|
||||
|
||||
//console.log.restore();
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
//console.log.restore();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed on try-catch.html', function(done) {
|
||||
this.timeout(15000);
|
||||
|
||||
var url = 'http://localhost:8388/try-catch.html';
|
||||
|
||||
ylt(url)
|
||||
.then(function(data) {
|
||||
console.log(data.toolsResults.phantomas.offenders.jsErrors);
|
||||
data.toolsResults.phantomas.metrics.should.have.a.property('jsErrors').that.equals(0);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
console.log.restore();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should take a screenshot', function(done) {
|
||||
this.timeout(15000);
|
||||
|
||||
var url = 'http://localhost:8388/simple-page.html';
|
||||
var screenshotPath = path.join(__dirname, '../../.tmp/indexTestScreenshot.png');
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, '../../.tmp'))){
|
||||
fs.mkdirSync(path.join(__dirname, '../../.tmp'));
|
||||
}
|
||||
|
||||
ylt(url, {screenshot: screenshotPath})
|
||||
.then(function(data) {
|
||||
|
||||
data.params.options.should.have.a.property('screenshot').that.equals(screenshotPath);
|
||||
data.should.not.have.a.property('screenshotUrl');
|
||||
|
||||
fs.existsSync(screenshotPath).should.equal(true);
|
||||
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
}).finally(function() {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
var should = require('chai').should();
|
||||
var mediaQueriesChecker = require('../../lib/tools/mediaQueriesChecker');
|
||||
|
||||
describe('mediaQueriesChecker', function() {
|
||||
|
||||
function wrap(message) {
|
||||
return {
|
||||
url: 'http://domain.com/css/stylesheet.css',
|
||||
value: {
|
||||
message: message,
|
||||
position: {
|
||||
start: {
|
||||
line: 269,
|
||||
column: 1
|
||||
},
|
||||
end: {
|
||||
line: 269,
|
||||
column: 182
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
it('should parse mediaQueryes correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)'))
|
||||
.should.deep.equal({
|
||||
mediaQuery: {
|
||||
query: 'screen and (max-width: 1024px)',
|
||||
rules: 1,
|
||||
file: 'http://domain.com/css/stylesheet.css',
|
||||
line: 269,
|
||||
column: 1
|
||||
},
|
||||
isForMobile: true,
|
||||
breakpoints: [{string: '1024px', pixels: 1024}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should determine if it\'s for mobile correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-width:1024px) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 320px) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 290px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 320px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 600px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 20em) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 40em) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 600px) and (max-width: 1000px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 180px) and (max-width: 400px) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 180px) and (max-width: 290px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 600px) (1 rules)')).isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 180px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules)')).isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules)')).isForMobile.should.equal(true);
|
||||
});
|
||||
|
||||
it('should count breakpoints correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)')).breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-width:1024px) (1 rules)')).breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 320px) (1 rules)')).breakpoints.should.deep.equal([{string: '320px', pixels: 320}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 600px) and (max-width: 1000px) (1 rules)')).breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 180px) (1 rules)')).breakpoints.should.deep.equal([{string: '180px', pixels: 180}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules)')).breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-height:500px) (1 rules)')).breakpoints.should.deep.equal([]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 100em) (1 rules)')).breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 120pt) (1 rules)')).breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 40.2em) (1 rules)')).breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]);
|
||||
});
|
||||
|
||||
it('should fail silently', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery(wrap('@media bad syntax (1 rules)')).isForMobile.should.equal(false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,323 @@
|
||||
var should = require('chai').should();
|
||||
var offendersHelpers = require('../../lib/offendersHelpers');
|
||||
|
||||
describe('offendersHelpers', function() {
|
||||
|
||||
describe('domPathToArray', function() {
|
||||
|
||||
it('should transform a path to an array', function() {
|
||||
var result = offendersHelpers.domPathToArray('body > section#page > div.alternate-color > ul.retroGuide > li[0] > div.retro-chaine.france2');
|
||||
result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']);
|
||||
});
|
||||
|
||||
it('should work even if a space is missing', function() {
|
||||
var result = offendersHelpers.domPathToArray('body > section#page> div.alternate-color > ul.retroGuide >li[0] > div.retro-chaine.france2');
|
||||
result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('listOfDomArraysToTree', function() {
|
||||
|
||||
it('should transform a list of arrays into a tree', function() {
|
||||
var input = [
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'],
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'],
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[1]', 'div.retro-chaine.france2']
|
||||
];
|
||||
|
||||
var inputClone = input.slice();
|
||||
|
||||
var result = offendersHelpers.listOfDomArraysToTree(input);
|
||||
result.should.deep.equal({
|
||||
'body': {
|
||||
'section#page': {
|
||||
'div.alternate-color': {
|
||||
'ul.retroGuide': {
|
||||
'li[0]': {
|
||||
'div.retro-chaine.france2': 2
|
||||
},
|
||||
'li[1]': {
|
||||
'div.retro-chaine.france2': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.should.deep.equal(inputClone);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('domPathToDomElementObj', function() {
|
||||
|
||||
it('should transform html', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('html');
|
||||
result.should.deep.equal({
|
||||
type: 'html'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform body', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('body');
|
||||
result.should.deep.equal({
|
||||
type: 'body'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform head', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('head');
|
||||
result.should.deep.equal({
|
||||
type: 'head'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform #document', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('#document');
|
||||
result.should.deep.equal({
|
||||
type: 'document'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform window', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('window');
|
||||
result.should.deep.equal({
|
||||
type: 'window'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a standard in-body element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('body > div#colorbox > div#cboxContent');
|
||||
result.should.deep.equal({
|
||||
type: 'domElement',
|
||||
element: 'div#cboxContent',
|
||||
tree: {
|
||||
'body': {
|
||||
'div#colorbox': {
|
||||
'div#cboxContent': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a domFragment element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('DocumentFragment');
|
||||
result.should.deep.equal({
|
||||
type: 'fragment'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a domFragment element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('DocumentFragment > div#colorbox > div#cboxContent');
|
||||
result.should.deep.equal({
|
||||
type: 'fragmentElement',
|
||||
element: 'div#cboxContent',
|
||||
tree: {
|
||||
'DocumentFragment': {
|
||||
'div#colorbox': {
|
||||
'div#cboxContent': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform an not-attached element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('div#sizcache');
|
||||
result.should.deep.equal({
|
||||
type: 'createdElement',
|
||||
element: 'div#sizcache'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform an not-attached element path', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('div > div#sizcache');
|
||||
result.should.deep.equal({
|
||||
type: 'createdElement',
|
||||
element: 'div#sizcache',
|
||||
tree: {
|
||||
'div': {
|
||||
'div#sizcache': 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('backtraceToArray', function() {
|
||||
|
||||
it('should transform a backtrace into an array', function() {
|
||||
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 / callback http://pouet.com/js/main.js:1');
|
||||
|
||||
result.should.deep.equal([
|
||||
{
|
||||
file: 'http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
functionName: 'callback',
|
||||
file: 'http://pouet.com/js/main.js',
|
||||
line: 1
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transform another backtrace syntax into an array', function() {
|
||||
var result = offendersHelpers.backtraceToArray('phantomjs://webpage.evaluate():38 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / a (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:3 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:8');
|
||||
|
||||
result.should.deep.equal([
|
||||
{
|
||||
file: 'phantomjs://webpage.evaluate()',
|
||||
line: 38
|
||||
},
|
||||
{
|
||||
functionName: 'e',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
functionName: 'a',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 3
|
||||
},
|
||||
{
|
||||
functionName: 'e',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 8
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transform a backtrace with the new PhantomJS 2.x syntax into an array', function() {
|
||||
var result = offendersHelpers.backtraceToArray('each@http://m.australia.fr/js/min/vendors.js?20160706185900:4:5365 / f@http://m.australia.fr/js/min/vendors.js?20160706185900:17:82 / http://m.australia.fr/js/min/vendors.js?20160706185900:17:855 / handle@http://m.australia.fr/js/min/vendors.js?20160706185900:5:10871 / report@phantomjs://platform/phantomas.js:535:20 / phantomjs://platform/phantomas.js:524:15');
|
||||
|
||||
result.should.deep.equal([
|
||||
{
|
||||
functionName: 'each',
|
||||
file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
|
||||
line: 4,
|
||||
column: 5365
|
||||
},
|
||||
{
|
||||
functionName: 'f',
|
||||
file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
|
||||
line: 17,
|
||||
column: 82
|
||||
},
|
||||
{
|
||||
file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
|
||||
line: 17,
|
||||
column: 855
|
||||
},
|
||||
{
|
||||
functionName: 'handle',
|
||||
file: 'http://m.australia.fr/js/min/vendors.js?20160706185900',
|
||||
line: 5,
|
||||
column: 10871
|
||||
},
|
||||
{
|
||||
functionName: 'report',
|
||||
file: 'phantomjs://platform/phantomas.js',
|
||||
line: 535,
|
||||
column: 20
|
||||
},
|
||||
{
|
||||
file: 'phantomjs://platform/phantomas.js',
|
||||
line: 524,
|
||||
column: 15
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return null if it fails', function() {
|
||||
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
|
||||
|
||||
should.equal(result, null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('sortVarsLikeChromeDevTools', function() {
|
||||
|
||||
it('should sort in the same strange order', function() {
|
||||
var result = offendersHelpers.sortVarsLikeChromeDevTools([
|
||||
'a',
|
||||
'aaa',
|
||||
'a2',
|
||||
'b',
|
||||
'A',
|
||||
'AAA',
|
||||
'B',
|
||||
'_a',
|
||||
'_aaa',
|
||||
'__a',
|
||||
'a_a',
|
||||
'aA',
|
||||
'a__',
|
||||
'$',
|
||||
'$a'
|
||||
]);
|
||||
|
||||
result.should.deep.equal([
|
||||
'$',
|
||||
'$a',
|
||||
'A',
|
||||
'AAA',
|
||||
'B',
|
||||
'__a',
|
||||
'_a',
|
||||
'_aaa',
|
||||
'a',
|
||||
'a2',
|
||||
'aA',
|
||||
'a__',
|
||||
'a_a',
|
||||
'aaa',
|
||||
'b'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('urlToLink', function() {
|
||||
|
||||
it('should transform an url into an html link', function() {
|
||||
var result = offendersHelpers.urlToLink('http://www.google.com/js/main.js');
|
||||
|
||||
result.should.equal('<a href="http://www.google.com/js/main.js" target="_blank" title="http://www.google.com/js/main.js">http://www.google.com/js/main.js</a>');
|
||||
});
|
||||
|
||||
it('should ellypsis the url if too long', function() {
|
||||
var result = offendersHelpers.urlToLink('http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js');
|
||||
|
||||
result.should.equal('<a href="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js" target="_blank" title="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js">http://www.google.com/js/longlonglonglonglonglo ... longlonglonglonglonglonglonglonglonglong/main.js</a>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fileWithSizePattern', function() {
|
||||
|
||||
it('should return an object', function() {
|
||||
var result = offendersHelpers.fileWithSizePattern('http://img3.pouet.com/2008/portail/js/jq-timer.js (1.72 kB)');
|
||||
|
||||
result.should.deep.equal({
|
||||
file: 'http://img3.pouet.com/2008/portail/js/jq-timer.js',
|
||||
size: 1.72
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
var should = require('chai').should();
|
||||
var phantomasWrapper = require('../../lib/tools/phantomas/phantomasWrapper');
|
||||
|
||||
describe('phantomasWrapper', function() {
|
||||
|
||||
it('should have a method execute', function() {
|
||||
phantomasWrapper.should.have.property('execute').that.is.a('function');
|
||||
});
|
||||
|
||||
it('should execute', function(done) {
|
||||
var url = 'http://localhost:8388/simple-page.html';
|
||||
|
||||
this.timeout(15000);
|
||||
|
||||
phantomasWrapper.execute({
|
||||
params: {
|
||||
url: url,
|
||||
options: {}
|
||||
}
|
||||
}).then(function(data) {
|
||||
/*jshint -W030 */
|
||||
|
||||
data.should.be.an('object');
|
||||
data.should.have.a.property('generator');
|
||||
data.generator.should.contain('phantomas');
|
||||
data.should.have.a.property('url').that.equals(url);
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty;
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty;
|
||||
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when page cannot be fetched', function(done) {
|
||||
var url = 'http://localhost:8389/not-existing.html';
|
||||
|
||||
this.timeout(15000);
|
||||
|
||||
phantomasWrapper.execute({
|
||||
params: {
|
||||
url: url,
|
||||
options: {
|
||||
device: 'desktop'
|
||||
}
|
||||
}
|
||||
}).then(function(data) {
|
||||
|
||||
done('Error: unwanted success');
|
||||
|
||||
}).fail(function(err) {
|
||||
try {
|
||||
console.log(err);
|
||||
should.exist(err);
|
||||
|
||||
done();
|
||||
} catch(error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
var should = require('chai').should();
|
||||
var redownload = require('../../lib/tools/redownload/redownload');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('redownload', function() {
|
||||
|
||||
it('should download a list of files', function(done) {
|
||||
this.timeout(10000);
|
||||
|
||||
var requestsList = [
|
||||
{
|
||||
url: 'http://localhost:8388/simple-page.html',
|
||||
type: 'html',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
type: 'js',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/jpeg-image.jpg',
|
||||
type: 'image',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
type: 'image',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
type: 'js',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
type: 'css',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8388/xml.xml',
|
||||
type: 'xml',
|
||||
size: 30000
|
||||
},
|
||||
{
|
||||
url: 'about:blank'
|
||||
}
|
||||
];
|
||||
|
||||
var data = {
|
||||
params: {
|
||||
options: {
|
||||
device: 'phone'
|
||||
}
|
||||
},
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
metrics: {
|
||||
requests: 8
|
||||
},
|
||||
offenders: {
|
||||
requests: requestsList
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
redownload.recheckAllFiles(data)
|
||||
|
||||
.then(function(data) {
|
||||
data.toolsResults.should.have.a.property('redownload');
|
||||
data.toolsResults.redownload.should.have.a.property('metrics');
|
||||
data.toolsResults.redownload.should.have.a.property('offenders');
|
||||
|
||||
data.toolsResults.redownload.offenders.should.have.a.property('totalWeight');
|
||||
data.toolsResults.redownload.offenders.totalWeight.totalWeight.should.be.above(0);
|
||||
data.toolsResults.redownload.offenders.totalWeight.byType.html.requests.length.should.equal(1);
|
||||
data.toolsResults.redownload.offenders.totalWeight.byType.js.requests.length.should.equal(2);
|
||||
data.toolsResults.redownload.offenders.totalWeight.byType.css.requests.length.should.equal(1);
|
||||
data.toolsResults.redownload.offenders.totalWeight.byType.image.requests.length.should.equal(2);
|
||||
data.toolsResults.redownload.offenders.totalWeight.byType.other.requests.length.should.equal(1);
|
||||
|
||||
data.toolsResults.redownload.offenders.should.have.a.property('compression');
|
||||
data.toolsResults.redownload.offenders.compression.totalGain.should.be.above(0);
|
||||
data.toolsResults.redownload.offenders.compression.files.length.should.equal(5);
|
||||
|
||||
data.toolsResults.redownload.offenders.should.have.a.property('fileMinification');
|
||||
data.toolsResults.redownload.offenders.fileMinification.totalGain.should.be.above(0);
|
||||
data.toolsResults.redownload.offenders.fileMinification.files.length.should.equal(2);
|
||||
|
||||
data.toolsResults.redownload.metrics.should.have.a.property('totalRequests').that.equals(7);
|
||||
data.toolsResults.redownload.offenders.should.have.a.property('totalRequests');
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.html.length.should.equal(1);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.js.length.should.equal(2);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.css.length.should.equal(1);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.image.length.should.equal(2);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.json.length.should.equal(0);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.webfont.length.should.equal(0);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.video.length.should.equal(0);
|
||||
data.toolsResults.redownload.offenders.totalRequests.byType.other.length.should.equal(1);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should download a file and measure its size', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
redownload.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
|
||||
newEntry.weightCheck.bodySize.should.equal(93636);
|
||||
newEntry.weightCheck.uncompressedSize.should.equal(newEntry.weightCheck.bodySize);
|
||||
newEntry.weightCheck.isCompressed.should.equal(false);
|
||||
newEntry.weightCheck.headersSize.should.be.above(200).and.below(400);
|
||||
newEntry.weightCheck.bodyBuffer.toString().should.have.string('1.8.3');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should download a PNG image and find the same body as fs.readFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/logo-large.png'));
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/logo-large.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
contentType: 'image/png'
|
||||
};
|
||||
|
||||
redownload.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
|
||||
newEntry.weightCheck.bodySize.should.equal(4193);
|
||||
newEntry.weightCheck.bodyBuffer.should.deep.equal(fileContent);
|
||||
|
||||
// Opening the image in jimp to check if the format is good
|
||||
var Jimp = require('jimp');
|
||||
Jimp.read(newEntry.weightCheck.bodyBuffer, function(err, image) {
|
||||
image.bitmap.width.should.equal(620);
|
||||
image.bitmap.height.should.equal(104);
|
||||
done(err);
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail downloading a file in error', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/no-file',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
contentLength: 999
|
||||
};
|
||||
|
||||
redownload.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('message').that.equals('error while downloading: 404');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not download a non 200 request', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/no-file',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 302,
|
||||
isHTML: true,
|
||||
contentLength: 999,
|
||||
notFound: true
|
||||
};
|
||||
|
||||
redownload.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.should.not.have.a.property('weightCheck');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should listRequestWeight', function() {
|
||||
var totalWeightObj = redownload.listRequestWeight([{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
bodyBuffer: 'blabla',
|
||||
headersSize: 200,
|
||||
bodySize: 500,
|
||||
isCompressed: false,
|
||||
uncompressedSize: 500
|
||||
}
|
||||
}]);
|
||||
|
||||
totalWeightObj.totalWeight.should.equal(700);
|
||||
totalWeightObj.byType.html.totalWeight.should.equal(700);
|
||||
totalWeightObj.byType.html.requests.should.have.length(1);
|
||||
totalWeightObj.byType.html.requests[0].should.have.a.property('url').that.equals('http://localhost:8388/jquery1.8.3.js');
|
||||
totalWeightObj.byType.html.requests[0].should.have.a.property('weight').that.equals(700);
|
||||
});
|
||||
|
||||
it('should listRequestWeight even if download failed', function() {
|
||||
var totalWeightObj = redownload.listRequestWeight([{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
message: 'error while downloading: 404'
|
||||
}
|
||||
}]);
|
||||
|
||||
totalWeightObj.totalWeight.should.equal(999);
|
||||
});
|
||||
|
||||
});
|
||||