Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e795b3369d | ||
|
|
2f26c7db96 | ||
|
|
ed588ba43c | ||
|
|
c9f63138a0 | ||
|
|
97f779dc83 | ||
|
|
2753446a57 | ||
|
|
dd4d0d4607 | ||
|
|
88bbaea4c9 | ||
|
|
f7280c1a90 | ||
|
|
bfd6c5d2bd | ||
|
|
a579f5736b | ||
|
|
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 |
@@ -1,9 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
package-lock.json
|
||||||
.tmp
|
.tmp
|
||||||
.vagrant
|
tmp
|
||||||
results/*
|
|
||||||
coverage
|
coverage
|
||||||
front/build
|
package-lock.json
|
||||||
|
|
||||||
har.json
|
har.json
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "0.10.33"
|
|
||||||
before_install:
|
|
||||||
- "npm install -g grunt-cli"
|
|
||||||
- "npm install -g phantomjs"
|
|
||||||
install: npm install
|
|
||||||
before_script: grunt test
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: gaelmetais
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
module.exports = function(grunt) {
|
|
||||||
|
|
||||||
// Load all grunt modules
|
|
||||||
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
|
|
||||||
|
|
||||||
// Tell our Express server that Grunt launched it
|
|
||||||
process.env.GRUNTED = true;
|
|
||||||
|
|
||||||
// Project configuration.
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
settings: grunt.file.readJSON('./server_config/settings.json'),
|
|
||||||
|
|
||||||
font: {
|
|
||||||
icons: {
|
|
||||||
src: ['front/src/fonts/svg-icons/*.svg'],
|
|
||||||
destCss: 'front/src/less/icons.less',
|
|
||||||
destFonts: 'front/src/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 '/fonts/' + fileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
less: {
|
|
||||||
all: {
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'front/src/less/',
|
|
||||||
src: ['**/*.less'],
|
|
||||||
dest: 'front/src/css/',
|
|
||||||
ext: '.css'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
replace: {
|
|
||||||
dist: {
|
|
||||||
options: {
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
match: 'googleAnalyticsId',
|
|
||||||
replacement: '<%= settings.googleAnalyticsId %>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: 'version',
|
|
||||||
replacement: 'v<%= pkg.version %>'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
{expand: true, flatten: true, src: ['front/src/main.html'], dest: 'front/build/'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
jshint: {
|
|
||||||
all: [
|
|
||||||
'*.js',
|
|
||||||
'app/lib/*.js',
|
|
||||||
'bin/*.js',
|
|
||||||
'lib/**/*.js',
|
|
||||||
'app/nodeControllers/*.js',
|
|
||||||
'app/public/scripts/*.js',
|
|
||||||
'phantomas_custom/**/*.js',
|
|
||||||
'test/**/*.js',
|
|
||||||
'front/src/js/**/*.js'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
clean: {
|
|
||||||
tmp: {
|
|
||||||
src: ['.tmp']
|
|
||||||
},
|
|
||||||
dev: {
|
|
||||||
src: ['front/src/css']
|
|
||||||
},
|
|
||||||
coverage: {
|
|
||||||
src: ['.tmp', 'coverage/']
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
src: ['front/build']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
copy: {
|
|
||||||
beforeCoverage: {
|
|
||||||
files: [
|
|
||||||
{src: ['bin/server.js'], dest: '.tmp/'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
coverage: {
|
|
||||||
files: [
|
|
||||||
{src: ['test/**'], dest: 'coverage/'},
|
|
||||||
{src: ['lib/metadata/**'], dest: 'coverage/'},
|
|
||||||
{src: ['node_modules/phantomas/**'], dest: 'coverage/'},
|
|
||||||
{src: ['lib/tools/phantomas/custom_modules/**'], dest: 'coverage/'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
files: [
|
|
||||||
{src: ['./front/src/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
|
|
||||||
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
|
|
||||||
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lineremover: {
|
|
||||||
beforeCoverage: {
|
|
||||||
files: {
|
|
||||||
'.tmp/bin/cli.js': 'bin/cli.js'
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
exclusionPattern: /#!\/usr\/bin\/env node/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
blanket: {
|
|
||||||
coverageApp: {
|
|
||||||
src: ['app/'],
|
|
||||||
dest: 'coverage/app/'
|
|
||||||
},
|
|
||||||
coverageLib: {
|
|
||||||
src: ['lib/'],
|
|
||||||
dest: 'coverage/lib/'
|
|
||||||
},
|
|
||||||
coverageBin: {
|
|
||||||
src: ['.tmp/bin/'],
|
|
||||||
dest: 'coverage/bin/'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mochaTest: {
|
|
||||||
test: {
|
|
||||||
options: {
|
|
||||||
reporter: 'spec',
|
|
||||||
},
|
|
||||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
|
||||||
},
|
|
||||||
'test-current-work': {
|
|
||||||
options: {
|
|
||||||
reporter: 'spec',
|
|
||||||
},
|
|
||||||
src: ['test/core/offendersHelpersTest.js']
|
|
||||||
},
|
|
||||||
coverage: {
|
|
||||||
options: {
|
|
||||||
reporter: 'html-cov',
|
|
||||||
quiet: true,
|
|
||||||
captureFile: 'coverage/coverage.html'
|
|
||||||
},
|
|
||||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
dev: {
|
|
||||||
NODE_ENV: 'development'
|
|
||||||
},
|
|
||||||
builded: {
|
|
||||||
NODE_ENV: 'production'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
express: {
|
|
||||||
dev: {
|
|
||||||
options: {
|
|
||||||
port: 8383,
|
|
||||||
server: './bin/server.js',
|
|
||||||
serverreload: true,
|
|
||||||
showStack: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builded: {
|
|
||||||
options: {
|
|
||||||
port: 8383,
|
|
||||||
server: './bin/server.js',
|
|
||||||
serverreload: true,
|
|
||||||
showStack: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
options: {
|
|
||||||
port: 8387,
|
|
||||||
server: './coverage/bin/server.js',
|
|
||||||
showStack: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'test-current-work': {
|
|
||||||
options: {
|
|
||||||
port: 8387,
|
|
||||||
server: './bin/server.js',
|
|
||||||
showStack: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
testSuite: {
|
|
||||||
options: {
|
|
||||||
port: 8388,
|
|
||||||
bases: 'test/www'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
useminPrepare: {
|
|
||||||
html: './front/src/main.html',
|
|
||||||
options: {
|
|
||||||
dest: './front/build',
|
|
||||||
root: ['./', './front/src']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
usemin: {
|
|
||||||
html: './front/build/main.html',
|
|
||||||
css: './front/build/css/*.css',
|
|
||||||
options: {
|
|
||||||
assetsDirs: ['front/build'],
|
|
||||||
patterns: {
|
|
||||||
css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
htmlmin: {
|
|
||||||
options: {
|
|
||||||
removeComments: true,
|
|
||||||
collapseWhitespace: true
|
|
||||||
},
|
|
||||||
main: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: './front/build/',
|
|
||||||
src: 'main.html',
|
|
||||||
flatten: true,
|
|
||||||
dest: './front/build'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
views: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: './front/src/views',
|
|
||||||
src: '*.html',
|
|
||||||
flatten: true,
|
|
||||||
dest: '.tmp/views/'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inline_angular_templates: {
|
|
||||||
build: {
|
|
||||||
options: {
|
|
||||||
base: '.tmp',
|
|
||||||
method: 'append'
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
'./front/build/main.html': ['.tmp/views/*.html']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filerev: {
|
|
||||||
options: {
|
|
||||||
algorithm: 'md5',
|
|
||||||
length: 8
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
src: './front/build/*/*.*'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Custom task: copies the test settings.json file to the coverage folder, and checks if there's no missing fields
|
|
||||||
grunt.registerTask('copy-test-server-settings', function() {
|
|
||||||
var mainSettingsFile = './server_config/settings.json';
|
|
||||||
var testSettingsFile = './test/fixtures/settings.json';
|
|
||||||
|
|
||||||
var mainSettings = grunt.file.readJSON(mainSettingsFile);
|
|
||||||
var testSettings = grunt.file.readJSON(testSettingsFile);
|
|
||||||
|
|
||||||
// Recursively compare keys of two objects (not the values)
|
|
||||||
function compareKeys(original, copy, context) {
|
|
||||||
for (var key in original) {
|
|
||||||
if (!copy[key] && copy[key] !== '' && copy[key] !== 0) {
|
|
||||||
grunt.fail.warn('Settings file ' + testSettingsFile + ' doesn\'t contain key ' + context + '.' + key);
|
|
||||||
}
|
|
||||||
if (original[key] !== null && typeof original[key] === 'object') {
|
|
||||||
compareKeys(original[key], copy[key], context + '.' + key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compareKeys(mainSettings, testSettings, 'settings');
|
|
||||||
|
|
||||||
var outputFile = './coverage/server_config/settings.json';
|
|
||||||
grunt.file.write(outputFile, JSON.stringify(testSettings, null, 4));
|
|
||||||
grunt.log.ok('File ' + outputFile + ' created');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
grunt.registerTask('icons', [
|
|
||||||
'font:icons',
|
|
||||||
'less',
|
|
||||||
'clean:tmp'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('build', [
|
|
||||||
'jshint',
|
|
||||||
'clean:build',
|
|
||||||
'copy:build',
|
|
||||||
'less',
|
|
||||||
'useminPrepare',
|
|
||||||
'concat',
|
|
||||||
'uglify',
|
|
||||||
'cssmin',
|
|
||||||
'replace',
|
|
||||||
'htmlmin:views',
|
|
||||||
'inline_angular_templates',
|
|
||||||
'filerev',
|
|
||||||
'usemin',
|
|
||||||
'htmlmin:main',
|
|
||||||
'clean:tmp'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('hint', [
|
|
||||||
'jshint'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('dev', [
|
|
||||||
'env:dev',
|
|
||||||
'express:dev'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('builded', [
|
|
||||||
'env:builded',
|
|
||||||
'express:builded'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('test', [
|
|
||||||
'build',
|
|
||||||
'express:testSuite',
|
|
||||||
'clean:coverage',
|
|
||||||
'copy-test-server-settings',
|
|
||||||
'lineremover:beforeCoverage',
|
|
||||||
'copy:beforeCoverage',
|
|
||||||
'blanket',
|
|
||||||
'copy:coverage',
|
|
||||||
'express:test',
|
|
||||||
'mochaTest:test',
|
|
||||||
'mochaTest:coverage',
|
|
||||||
'clean:tmp'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('test-current-work', [
|
|
||||||
'jshint',
|
|
||||||
'express:testSuite',
|
|
||||||
'clean:coverage',
|
|
||||||
'copy-test-server-settings',
|
|
||||||
'express:test-current-work',
|
|
||||||
'mochaTest:test-current-work',
|
|
||||||
'clean:tmp'
|
|
||||||
]);
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,57 +1,91 @@
|
|||||||
# Yellow Lab Tools
|
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
|
||||||
|
|
||||||
Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
|
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
|
||||||
|
|
||||||
[](https://gemnasium.com/gmetais/YellowLabTools) [](https://travis-ci.org/gmetais/YellowLabTools)
|
|
||||||
|
|
||||||
|
|
||||||
### 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
|
## How it works
|
||||||
|
|
||||||
The tool loads the given URL via [PhantomasJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
|
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.
|
||||||
|
|
||||||
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
|
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!!!
|
||||||
|
|
||||||
|

|
||||||
## Different ways to use YLT
|
|
||||||
|
|
||||||
#### The web interface:
|
|
||||||
This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).
|
|
||||||
Please note the server is hosted in Baltimore, USA.
|
|
||||||
|
|
||||||
#### The Command Line Client:
|
|
||||||
Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
|
|
||||||
|
|
||||||
#### The NPM module:
|
|
||||||
Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
|
|
||||||
|
|
||||||
#### The Public API:
|
|
||||||
Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
|
|
||||||
|
|
||||||
|
|
||||||
## Install your own private instance
|
## Install your own private 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-private-server).
|
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).
|
||||||
|
|
||||||
## Help needed
|
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)).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
Please read the [license](LICENSE).
|
|
||||||
Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
|
||||||
|
|
||||||
|
|
||||||
## Author
|
## 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
|
## Contributors
|
||||||
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
|
- 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,10 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
var debug = require('debug')('ylt:cli');
|
var debug = require('debug')('ylt:cli');
|
||||||
var meow = require('meow');
|
var meow = require('meow');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
var EasyXml = require('easyxml');
|
||||||
|
|
||||||
var ylt = require('../lib/index');
|
var ylt = require('../lib/index');
|
||||||
|
|
||||||
var cli = meow({
|
var cli = meow({
|
||||||
help: [
|
help: [
|
||||||
@@ -12,18 +13,27 @@ var cli = meow({
|
|||||||
' yellowlabtools <url> <options>',
|
' yellowlabtools <url> <options>',
|
||||||
'',
|
'',
|
||||||
'Options:',
|
'Options:',
|
||||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
' --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".',
|
||||||
|
//' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
|
||||||
|
' --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".',
|
||||||
''
|
''
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
pkg: '../package.json'
|
pkg: require('../package.json')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check parameters
|
// Check parameters
|
||||||
if (cli.input.length < 1) {
|
if (cli.input.length < 1) {
|
||||||
console.error('Incorrect parameters: url not provided');
|
cli.showHelp();
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = cli.input[0];
|
var url = cli.input[0];
|
||||||
@@ -44,6 +54,32 @@ if (screenshot) {
|
|||||||
options.screenshot = cli.flags.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;
|
||||||
|
|
||||||
|
// 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) {
|
(function execute(url, options) {
|
||||||
@@ -54,7 +90,34 @@ if (screenshot) {
|
|||||||
then(function(data) {
|
then(function(data) {
|
||||||
|
|
||||||
debug('Success');
|
debug('Success');
|
||||||
console.log(JSON.stringify(data, null, 2));
|
switch(cli.flags.reporter) {
|
||||||
|
case 'xml':
|
||||||
|
var serializer = new EasyXml({
|
||||||
|
manifest: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove some heavy parts of the results object
|
||||||
|
delete data.toolsResults;
|
||||||
|
delete data.javascriptExecutionTree;
|
||||||
|
|
||||||
|
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) {
|
}).fail(function(err) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
var express = require('express');
|
|
||||||
var app = express();
|
|
||||||
var server = require('http').createServer(app);
|
|
||||||
var bodyParser = require('body-parser');
|
|
||||||
var compress = require('compression');
|
|
||||||
var cors = require('cors');
|
|
||||||
|
|
||||||
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
|
|
||||||
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
|
|
||||||
|
|
||||||
|
|
||||||
// Middlewares
|
|
||||||
app.use(compress());
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(cors());
|
|
||||||
app.use(authMiddleware);
|
|
||||||
app.use(apiLimitsMiddleware);
|
|
||||||
|
|
||||||
|
|
||||||
// Initialize the controllers
|
|
||||||
var apiController = require('../lib/server/controllers/apiController')(app);
|
|
||||||
var frontController = require('../lib/server/controllers/frontController')(app);
|
|
||||||
|
|
||||||
|
|
||||||
// Let's start the server!
|
|
||||||
if (!process.env.GRUNTED) {
|
|
||||||
var settings = require('../server_config/settings.json');
|
|
||||||
server.listen(settings.serverPort, function() {
|
|
||||||
console.log('Listening on port %d', server.address().port);
|
|
||||||
|
|
||||||
// For the tests
|
|
||||||
if (server.startTests) {
|
|
||||||
server.startTests();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "yellowlabtools",
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "~1.3.8",
|
|
||||||
"angular-route": "~1.3.8",
|
|
||||||
"angular-resource": "~1.3.7",
|
|
||||||
"angular-sanitize": "~1.4.0-beta.0"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"angular": "~1.3.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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 |
@@ -1,10 +0,0 @@
|
|||||||
.about {
|
|
||||||
margin: 3em auto;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
.about p {
|
|
||||||
margin: 2em;
|
|
||||||
}
|
|
||||||
.about a {
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
.testedUrl {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.summary {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.summary .globalScore {
|
|
||||||
display: table;
|
|
||||||
width: 60%;
|
|
||||||
margin: 3em auto;
|
|
||||||
}
|
|
||||||
.summary .globalScore > div {
|
|
||||||
display: table-cell;
|
|
||||||
width: 50%;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .globalScore .globalGrade {
|
|
||||||
margin: 0.5 auto;
|
|
||||||
width: 2.5em;
|
|
||||||
height: 2.5em;
|
|
||||||
line-height: 2.5em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
font-size: 3em;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .globalScore .on100 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0.5em 0 1em;
|
|
||||||
}
|
|
||||||
.summary .globalScore .screenshotWrapper:hover {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
.summary .globalScore .screenshotWrapper:hover:after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1.25em;
|
|
||||||
height: 1.25em;
|
|
||||||
top: 0.7em;
|
|
||||||
left: 1.55em;
|
|
||||||
font-size: 3em;
|
|
||||||
color: #FFF;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
text-align: center;
|
|
||||||
content: "+";
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
.summary .notations {
|
|
||||||
display: table;
|
|
||||||
width: 80%;
|
|
||||||
margin: 0 10% 1.5em;
|
|
||||||
border-spacing: 1em;
|
|
||||||
}
|
|
||||||
.summary .notations > div {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
.summary .notations > div > div {
|
|
||||||
display: table-cell;
|
|
||||||
height: 2.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .category {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria {
|
|
||||||
font-weight: normal;
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
.summary .notations .A.categoryScore,
|
|
||||||
.summary .notations .B.categoryScore,
|
|
||||||
.summary .notations .C.categoryScore,
|
|
||||||
.summary .notations .D.categoryScore,
|
|
||||||
.summary .notations .E.categoryScore,
|
|
||||||
.summary .notations .F.categoryScore,
|
|
||||||
.summary .notations .NA.categoryScore {
|
|
||||||
width: 2.5em;
|
|
||||||
max-width: 2.5em;
|
|
||||||
min-width: 2.5em;
|
|
||||||
font-size: 2em;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.summary .notations .grade .A,
|
|
||||||
.summary .notations .grade .B,
|
|
||||||
.summary .notations .grade .C,
|
|
||||||
.summary .notations .grade .D,
|
|
||||||
.summary .notations .grade .E,
|
|
||||||
.summary .notations .grade .F,
|
|
||||||
.summary .notations .grade .NA {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
font-size: 1em;
|
|
||||||
color: transparent;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .table > div:hover > div {
|
|
||||||
background: #EBD8E2;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .table > div:hover > div.info {
|
|
||||||
background: #FFF;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .table > div:hover > div.info .icon-question {
|
|
||||||
color: #EBD8E2;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .grade {
|
|
||||||
width: 10%;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .label {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .result {
|
|
||||||
width: 18%;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .warning .label,
|
|
||||||
.summary .notations .warning .result,
|
|
||||||
.summary .notations .icon-warning {
|
|
||||||
color: #FF1919;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .info {
|
|
||||||
width: 2%;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
background: #FFF;
|
|
||||||
padding-left: 0.1em;
|
|
||||||
padding-right: 0.1em;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .icon-question {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
.summary .fromShare {
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
.summary .apiTip {
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-bottom: 4em;
|
|
||||||
color: #413;
|
|
||||||
}
|
|
||||||
.summary .apiTip a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.summary .tweet .tweetText {
|
|
||||||
color: #413;
|
|
||||||
background: #F2F2F2;
|
|
||||||
border: none;
|
|
||||||
width: 25em;
|
|
||||||
padding: 0.4em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-shadow: 0.05em 0.1em 0 0 #999;
|
|
||||||
}
|
|
||||||
.summary .tweet .tweetButton,
|
|
||||||
.summary .tweet .linkedinButton {
|
|
||||||
color: #413;
|
|
||||||
background: #F2F2F2;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.summary .tweet .tweetButton:hover,
|
|
||||||
.summary .tweet .linkedinButton:hover {
|
|
||||||
color: #F2F2F2;
|
|
||||||
background: #e74c3c;
|
|
||||||
}
|
|
||||||
.summary .tweet input {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "fontsmith-icons";
|
|
||||||
src: url("/fonts/icons.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
.promess {
|
|
||||||
padding: 0em 2em 3em;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
.url {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
.launchBtn {
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.launchBtn.disabled {
|
|
||||||
background: #deaca6;
|
|
||||||
}
|
|
||||||
.features {
|
|
||||||
display: table;
|
|
||||||
width: 50%;
|
|
||||||
margin: 8em auto 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #413;
|
|
||||||
}
|
|
||||||
.features > div {
|
|
||||||
width: 33.3%;
|
|
||||||
display: table-cell;
|
|
||||||
padding: 0 1.5em;
|
|
||||||
}
|
|
||||||
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,273 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "fontsmith-icons";
|
|
||||||
src: url("/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;
|
|
||||||
}
|
|
||||||
.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 .menuItem.back,
|
|
||||||
.resultsMenu .menuItem.restart {
|
|
||||||
color: #413;
|
|
||||||
border-color: #413;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
/* Grade colors */
|
|
||||||
.A {
|
|
||||||
/* green */
|
|
||||||
background: #0C4;
|
|
||||||
}
|
|
||||||
.B {
|
|
||||||
/* green */
|
|
||||||
background: #CD0;
|
|
||||||
}
|
|
||||||
.C {
|
|
||||||
/* yellow */
|
|
||||||
background: #FD2;
|
|
||||||
}
|
|
||||||
.D {
|
|
||||||
/* orange */
|
|
||||||
background: #FA2;
|
|
||||||
}
|
|
||||||
.E {
|
|
||||||
/* red */
|
|
||||||
background: #F60;
|
|
||||||
}
|
|
||||||
.F {
|
|
||||||
/* red */
|
|
||||||
background: #F22;
|
|
||||||
}
|
|
||||||
.NA {
|
|
||||||
/* Non applicable */
|
|
||||||
background: #CCC;
|
|
||||||
}
|
|
||||||
.board {
|
|
||||||
margin-top: 2em;
|
|
||||||
padding: 1em;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.backToDashboard {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.backToDashboard a {
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: block;
|
|
||||||
margin-top: 4em;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
a.linkButton {
|
|
||||||
font-size: 1em;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
margin: 0.5em;
|
|
||||||
line-height: 2em;
|
|
||||||
border: 0 solid;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.screenshotWrapper.desktop {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
border: 0.2em solid #AAA;
|
|
||||||
background: #000;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-top-left-radius: 0.4em;
|
|
||||||
border-top-right-radius: 0.4em;
|
|
||||||
}
|
|
||||||
.screenshotWrapper.desktop:before {
|
|
||||||
position: absolute;
|
|
||||||
width: 15em;
|
|
||||||
height: 0.6em;
|
|
||||||
bottom: -0.75em;
|
|
||||||
left: -1em;
|
|
||||||
background: #CCC;
|
|
||||||
border-bottom-left-radius: 0.2em;
|
|
||||||
border-bottom-right-radius: 0.2em;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
.screenshotWrapper.desktop:after {
|
|
||||||
position: absolute;
|
|
||||||
width: 0.4em;
|
|
||||||
height: 0.2em;
|
|
||||||
bottom: -0.55em;
|
|
||||||
left: 12.5em;
|
|
||||||
background: lime;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
.screenshotWrapper.desktop > div {
|
|
||||||
width: 12em;
|
|
||||||
height: 6.75em;
|
|
||||||
overflow: scroll;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.screenshotWrapper.desktop .screenshotImage {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.star {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.star span {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
padding: 3em;
|
|
||||||
color: #413;
|
|
||||||
}
|
|
||||||
.footer a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.footer .version {
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
/* 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: "\e004";
|
|
||||||
}
|
|
||||||
.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: "\e003";
|
|
||||||
}
|
|
||||||
.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-loop {
|
|
||||||
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-loop:before {
|
|
||||||
content: "\e002";
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.status {
|
|
||||||
margin-top: 2em;
|
|
||||||
font-size: 2.5em;
|
|
||||||
}
|
|
||||||
.statusSubMessage {
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-bottom: 6em;
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
.rule.board {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.rule .ruleTable {
|
|
||||||
display: table;
|
|
||||||
border-spacing: 1em;
|
|
||||||
width: 90%;
|
|
||||||
margin: 2em auto;
|
|
||||||
background: #f2f2f2;
|
|
||||||
border: 1px dashed #666;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
.rule .ruleTable > div {
|
|
||||||
display: table-cell;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.rule .ruleTable .left {
|
|
||||||
width: 33%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.rule .ruleTable .right {
|
|
||||||
width: 67%;
|
|
||||||
}
|
|
||||||
.rule .score {
|
|
||||||
font-size: 2.5em;
|
|
||||||
line-height: 2em;
|
|
||||||
height: 2em;
|
|
||||||
width: 2em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
margin: 0 auto 0.5em;
|
|
||||||
}
|
|
||||||
.rule .message {
|
|
||||||
width: 80%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.rule .message p {
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
.rule .message ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
.rule .message li:before {
|
|
||||||
content: '\25e6';
|
|
||||||
margin-right: 0.3em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
position: relative;
|
|
||||||
top: 0.1em;
|
|
||||||
}
|
|
||||||
.rule .warning {
|
|
||||||
width: 90%;
|
|
||||||
margin: -1em auto 2em;
|
|
||||||
background: #FEE;
|
|
||||||
border: 1px dashed #e74c3c;
|
|
||||||
color: #e74c3c;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
.rule .offendersTable {
|
|
||||||
display: table;
|
|
||||||
border-spacing: 0 0.25em;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-width: 10%;
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
.rule .offendersTable > div {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
.rule .offendersTable > div > div {
|
|
||||||
display: table-cell;
|
|
||||||
background: #f2f2f2;
|
|
||||||
padding: 0 1em;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.rule .offendersTable > div > div:hover {
|
|
||||||
background: #EBD8E2;
|
|
||||||
}
|
|
||||||
.rule .notFound {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
.rule .notFound h2 {
|
|
||||||
font-size: 3em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
background: #efe;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
margin: 0.2em 0;
|
|
||||||
border-radius: 0.4em;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton.opens {
|
|
||||||
padding-right: 0.75em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton.opens:after {
|
|
||||||
position: relative;
|
|
||||||
left: 0.5em;
|
|
||||||
content: '\25BC';
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton > div {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
background: inherit;
|
|
||||||
border-bottom-left-radius: 0.4em;
|
|
||||||
border-bottom-right-radius: 0.4em;
|
|
||||||
border-top: 1px solid #999;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton .domTree {
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton .domTree > div {
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton .domTree > div div {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton .backtrace,
|
|
||||||
.offenders .offenderButton .cssFileAndLine {
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton.opens.mouseOver {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
background: #ffe0cc;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.offenders .offenderButton.opens.mouseOver > div {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.offendersHtml {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.domTree div {
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
.domTree div span:only-child {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.domTree div span:only-child span {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.colorPalette {
|
|
||||||
width: 30em;
|
|
||||||
border: 2px solid #000;
|
|
||||||
text-align: left;
|
|
||||||
/* Checkerboard background */
|
|
||||||
background-color: #ddd;
|
|
||||||
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
|
|
||||||
background-size: 1em 1em;
|
|
||||||
background-position: 0 0, 0.5em 0.5em;
|
|
||||||
}
|
|
||||||
.colorPalette > div {
|
|
||||||
display: inline-block;
|
|
||||||
height: 2em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.colorPalette > div div {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 100%;
|
|
||||||
top: 100%;
|
|
||||||
background: #FFF;
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 2px solid #f1c40f;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
z-index: 3;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.colorPalette > div:hover div {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.colorPalette > div:hover:after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
background-color: inherit;
|
|
||||||
left: -0.2em;
|
|
||||||
top: -0.2em;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
border: 0.2em solid #f1c40f;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.screenshot.board {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.screenshot .screenshotWrapper {
|
|
||||||
font-size: 2.08333333333333em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
/* Timeline colors, related to Window Performances */
|
|
||||||
.execution {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
margin: 1em auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
min-width: 30em;
|
|
||||||
width: 30%;
|
|
||||||
border: 1px dotted #aaa;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.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 .icon-warning {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.8em;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.execution .icon-warning {
|
|
||||||
color: #e74c3c;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@@ -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 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 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></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="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,64 +0,0 @@
|
|||||||
var yltApp = angular.module('YellowLabTools', [
|
|
||||||
'ngRoute',
|
|
||||||
'ngSanitize',
|
|
||||||
'indexCtrl',
|
|
||||||
'dashboardCtrl',
|
|
||||||
'queueCtrl',
|
|
||||||
'ruleCtrl',
|
|
||||||
'screenshotCtrl',
|
|
||||||
'timelineCtrl',
|
|
||||||
'runsFactory',
|
|
||||||
'resultsFactory',
|
|
||||||
'apiService',
|
|
||||||
'menuService',
|
|
||||||
'gradeDirective',
|
|
||||||
'offendersDirectives'
|
|
||||||
]);
|
|
||||||
|
|
||||||
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
|
|
||||||
$rootScope.loadedRunId = null;
|
|
||||||
|
|
||||||
// Google Analytics
|
|
||||||
$rootScope.$on('$routeChangeSuccess', function(){
|
|
||||||
ga('send', 'pageview', {'page': $location.path()});
|
|
||||||
});
|
|
||||||
}]);
|
|
||||||
|
|
||||||
yltApp.config(['$routeProvider', '$locationProvider',
|
|
||||||
function($routeProvider, $locationProvider) {
|
|
||||||
$routeProvider.
|
|
||||||
when('/', {
|
|
||||||
templateUrl: 'views/index.html',
|
|
||||||
controller: 'IndexCtrl'
|
|
||||||
}).
|
|
||||||
when('/queue/:runId', {
|
|
||||||
templateUrl: 'views/queue.html',
|
|
||||||
controller: 'QueueCtrl'
|
|
||||||
}).
|
|
||||||
when('/about', {
|
|
||||||
templateUrl: 'views/about.html'
|
|
||||||
}).
|
|
||||||
when('/result/:runId', {
|
|
||||||
templateUrl: 'views/dashboard.html',
|
|
||||||
controller: 'DashboardCtrl'
|
|
||||||
}).
|
|
||||||
when('/result/:runId/timeline', {
|
|
||||||
templateUrl: 'views/timeline.html',
|
|
||||||
controller: 'TimelineCtrl'
|
|
||||||
}).
|
|
||||||
when('/result/:runId/screenshot', {
|
|
||||||
templateUrl: 'views/screenshot.html',
|
|
||||||
controller: 'ScreenshotCtrl'
|
|
||||||
}).
|
|
||||||
when('/result/:runId/rule/:policy', {
|
|
||||||
templateUrl: 'views/rule.html',
|
|
||||||
controller: 'RuleCtrl'
|
|
||||||
}).
|
|
||||||
otherwise({
|
|
||||||
redirectTo: '/'
|
|
||||||
});
|
|
||||||
|
|
||||||
$locationProvider.html5Mode(true);
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
|
|
||||||
|
|
||||||
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
|
|
||||||
$scope.runId = $routeParams.runId;
|
|
||||||
$scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
|
|
||||||
$scope.fromSocialShare = $location.search().share;
|
|
||||||
|
|
||||||
function loadResults() {
|
|
||||||
// Load result if needed
|
|
||||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
|
||||||
Results.get({runId: $routeParams.runId}, function(result) {
|
|
||||||
$rootScope.loadedResult = result;
|
|
||||||
$scope.result = result;
|
|
||||||
init();
|
|
||||||
}, function(err) {
|
|
||||||
$scope.error = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.result = $rootScope.loadedResult;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
// By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
|
|
||||||
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
|
|
||||||
|
|
||||||
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
|
|
||||||
$scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.showRulePage = function(ruleName) {
|
|
||||||
$location.path('/result/' + $scope.runId + '/rule/' + ruleName);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.testAgain = function() {
|
|
||||||
API.launchTest($scope.result.params.url);
|
|
||||||
};
|
|
||||||
|
|
||||||
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
|
|
||||||
$scope.seeFullReport = function() {
|
|
||||||
$scope.fromSocialShare = false;
|
|
||||||
$location.search({});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.shareOnTwitter = function(message) {
|
|
||||||
openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.shareOnLinkedin = function(message) {
|
|
||||||
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
|
|
||||||
};
|
|
||||||
|
|
||||||
function openSocialPopup(url) {
|
|
||||||
var winHeight = 400;
|
|
||||||
var winWidth = 600;
|
|
||||||
var winTop = (screen.height / 2) - (winHeight / 2);
|
|
||||||
var winLeft = (screen.width / 2) - (winWidth / 2);
|
|
||||||
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the URL of the JSON result
|
|
||||||
$scope.getAPIUrl = function() {
|
|
||||||
return '/api/results/' + $scope.runId;
|
|
||||||
};
|
|
||||||
|
|
||||||
loadResults();
|
|
||||||
}]);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
var indexCtrl = angular.module('indexCtrl', []);
|
|
||||||
|
|
||||||
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
|
|
||||||
$scope.launchTest = function() {
|
|
||||||
if ($scope.url) {
|
|
||||||
API.launchTest($scope.url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
|
|
||||||
|
|
||||||
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
|
|
||||||
$scope.runId = $routeParams.runId;
|
|
||||||
|
|
||||||
var numberOfTries = 0;
|
|
||||||
|
|
||||||
function getRunStatus () {
|
|
||||||
Runs.get({runId: $scope.runId}, function(data) {
|
|
||||||
$scope.url = data.params.url;
|
|
||||||
$scope.status = data.status;
|
|
||||||
$scope.notFound = false;
|
|
||||||
$scope.connectionLost = false;
|
|
||||||
|
|
||||||
if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
|
|
||||||
numberOfTries ++;
|
|
||||||
|
|
||||||
// Retrying in 2 seconds (and increasing the delay a bit more each time)
|
|
||||||
setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
|
|
||||||
|
|
||||||
} else if (data.status.statusCode === 'complete') {
|
|
||||||
$location.path('/result/' + $scope.runId).replace();
|
|
||||||
} else {
|
|
||||||
// Handled by the view
|
|
||||||
}
|
|
||||||
}, function(response) {
|
|
||||||
if (response.status === 404) {
|
|
||||||
$scope.notFound = true;
|
|
||||||
$scope.connectionLost = false;
|
|
||||||
} else if (response.status === 0) {
|
|
||||||
// Connection lost, retry in 10 seconds
|
|
||||||
setTimeout(getRunStatus, 10000);
|
|
||||||
$scope.connectionLost = true;
|
|
||||||
$scope.notFound = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getRunStatus();
|
|
||||||
}]);
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
var ruleCtrl = angular.module('ruleCtrl', []);
|
|
||||||
|
|
||||||
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
|
|
||||||
$scope.runId = $routeParams.runId;
|
|
||||||
$scope.policyName = $routeParams.policy;
|
|
||||||
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
|
|
||||||
$scope.rule = null;
|
|
||||||
|
|
||||||
function loadResults() {
|
|
||||||
// Load result if needed
|
|
||||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
|
||||||
Results.get({runId: $routeParams.runId}, function(result) {
|
|
||||||
$rootScope.loadedResult = result;
|
|
||||||
$scope.result = result;
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.result = $rootScope.loadedResult;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
$scope.rule = $scope.result.rules[$scope.policyName];
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.backToDashboard = function() {
|
|
||||||
$location.path('/result/' + $scope.runId);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.testAgain = function() {
|
|
||||||
API.launchTest($scope.result.params.url);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadResults();
|
|
||||||
}]);
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
|
|
||||||
|
|
||||||
screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
|
|
||||||
$scope.runId = $routeParams.runId;
|
|
||||||
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
|
|
||||||
|
|
||||||
function loadResults() {
|
|
||||||
// Load result if needed
|
|
||||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
|
||||||
Results.get({runId: $routeParams.runId}, function(result) {
|
|
||||||
$rootScope.loadedResult = result;
|
|
||||||
$scope.result = result;
|
|
||||||
init();
|
|
||||||
}, function(err) {
|
|
||||||
$scope.error = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.result = $rootScope.loadedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.backToDashboard = function() {
|
|
||||||
$location.path('/result/' + $scope.runId);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.testAgain = function() {
|
|
||||||
API.launchTest($scope.result.params.url);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadResults();
|
|
||||||
}]);
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
var timelineCtrl = angular.module('timelineCtrl', []);
|
|
||||||
|
|
||||||
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
|
|
||||||
$scope.runId = $routeParams.runId;
|
|
||||||
$scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
|
|
||||||
|
|
||||||
function loadResults() {
|
|
||||||
// Load result if needed
|
|
||||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
|
||||||
Results.get({runId: $routeParams.runId}, function(result) {
|
|
||||||
$rootScope.loadedResult = result;
|
|
||||||
$scope.result = result;
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.result = $rootScope.loadedResult;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
initExecutionTree();
|
|
||||||
initTimeline();
|
|
||||||
$timeout(initProfiler, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initExecutionTree() {
|
|
||||||
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
|
|
||||||
$scope.executionTree = [];
|
|
||||||
|
|
||||||
originalExecutions.forEach(function(node) {
|
|
||||||
|
|
||||||
// Prepare a faster angular search by creating a kind of search index
|
|
||||||
node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
|
|
||||||
|
|
||||||
$scope.executionTree.push(node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTimeline() {
|
|
||||||
|
|
||||||
// Split the timeline into 200 intervals
|
|
||||||
var numberOfIntervals = 199;
|
|
||||||
var lastEvent = $scope.executionTree[$scope.executionTree.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
|
|
||||||
$scope.executionTree.forEach(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 initProfiler() {
|
|
||||||
$scope.profilerData = $scope.executionTree;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.filter = function(textFilter, scriptName) {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.onNodeDetailsClick = function(node) {
|
|
||||||
var isOpen = node.showDetails;
|
|
||||||
if (!isOpen) {
|
|
||||||
// Close all other nodes
|
|
||||||
$scope.executionTree.forEach(function(currentNode) {
|
|
||||||
currentNode.showDetails = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse the backtrace
|
|
||||||
if (!node.parsedBacktrace) {
|
|
||||||
node.parsedBacktrace = parseBacktrace(node.data.backtrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
node.showDetails = !isOpen;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.backToDashboard = function() {
|
|
||||||
$location.path('/result/' + $scope.runId);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.testAgain = function() {
|
|
||||||
API.launchTest($scope.result.params.url);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadResults();
|
|
||||||
|
|
||||||
}]);
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
var gradeDirective = angular.module('gradeDirective', []);
|
|
||||||
|
|
||||||
gradeDirective.directive('grade', function() {
|
|
||||||
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
score: '=score'
|
|
||||||
},
|
|
||||||
template: '<div ng-class="getGrade(score)">{{getGrade(score)}}</div>',
|
|
||||||
replace: true,
|
|
||||||
controller : ['$scope', function($scope) {
|
|
||||||
$scope.getGrade = function(score) {
|
|
||||||
if (score > 80) {
|
|
||||||
return 'A';
|
|
||||||
}
|
|
||||||
if (score > 60) {
|
|
||||||
return 'B';
|
|
||||||
}
|
|
||||||
if (score > 40) {
|
|
||||||
return 'C';
|
|
||||||
}
|
|
||||||
if (score > 20) {
|
|
||||||
return 'D';
|
|
||||||
}
|
|
||||||
if (score > 0) {
|
|
||||||
return 'E';
|
|
||||||
}
|
|
||||||
return 'F';
|
|
||||||
};
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
var offendersDirectives = angular.module('offendersDirectives', []);
|
|
||||||
|
|
||||||
offendersDirectives.directive('domTree', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
tree: '='
|
|
||||||
},
|
|
||||||
template: '<div class="domTree"></div>',
|
|
||||||
replace: true,
|
|
||||||
link: function(scope, element, attrs) {
|
|
||||||
|
|
||||||
function recursiveHtmlBuilder(tree) {
|
|
||||||
var html = '';
|
|
||||||
var keys = Object.keys(tree);
|
|
||||||
|
|
||||||
keys.forEach(function(key) {
|
|
||||||
if (isNaN(tree[key])) {
|
|
||||||
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
|
|
||||||
} else if (tree[key] > 1) {
|
|
||||||
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
|
|
||||||
} else {
|
|
||||||
html += '<div><span>' + key + '</span></div>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.append(recursiveHtmlBuilder(scope.tree));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('domElementButton', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
obj: '='
|
|
||||||
},
|
|
||||||
templateUrl: 'views/domElementButton.html',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.filter('shortenUrl', function() {
|
|
||||||
return function(url, maxLength) {
|
|
||||||
if (!maxLength) {
|
|
||||||
maxLength = 110;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Why dividing by 2.1? Because it adds a 5% margin.
|
|
||||||
var leftLength = Math.floor((maxLength - 5) / 2.1);
|
|
||||||
var rightLength = Math.ceil((maxLength - 5) / 2.1);
|
|
||||||
|
|
||||||
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('urlLink', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
url: '=',
|
|
||||||
maxLength: '='
|
|
||||||
},
|
|
||||||
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.filter('encodeURIComponent', function() {
|
|
||||||
return window.encodeURIComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('fileAndLine', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
file: '=',
|
|
||||||
line: '=',
|
|
||||||
column: '='
|
|
||||||
},
|
|
||||||
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file"><inline CSS></span> @ {{line}}:{{column}}</span>',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('fileAndLineButton', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
|
||||||
file: '=',
|
|
||||||
line: '=',
|
|
||||||
column: '='
|
|
||||||
},
|
|
||||||
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('offenderButton', function() {
|
|
||||||
return {
|
|
||||||
restrict: 'C',
|
|
||||||
link: function(scope, element, attrs) {
|
|
||||||
|
|
||||||
console.log('initializing touchstart');
|
|
||||||
|
|
||||||
element.bind('touchstart mouseenter', function(e) {
|
|
||||||
element.addClass('mouseOver');
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
element.bind('touchend mouseleave click', function(e) {
|
|
||||||
element.removeClass('mouseOver');
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
|
|
||||||
|
|
||||||
resultsFactory.factory('Results', ['$resource', function($resource) {
|
|
||||||
return $resource('/api/results/:runId', {
|
|
||||||
|
|
||||||
});
|
|
||||||
}]);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
var runsFactory = angular.module('runsFactory', ['ngResource']);
|
|
||||||
|
|
||||||
runsFactory.factory('Runs', ['$resource', function($resource) {
|
|
||||||
return $resource('/api/runs/:runId', {
|
|
||||||
|
|
||||||
});
|
|
||||||
}]);
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
var apiService = angular.module('apiService', []);
|
|
||||||
|
|
||||||
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
launchTest: function(url) {
|
|
||||||
Runs.save({
|
|
||||||
url: url,
|
|
||||||
waitForResponse: false,
|
|
||||||
screenshot: true
|
|
||||||
}, function(data) {
|
|
||||||
$location.path('/queue/' + data.runId);
|
|
||||||
}, function(response) {
|
|
||||||
if (response.status === 429) {
|
|
||||||
alert('Too many requests, you reached the max number of requests allowed in 24h');
|
|
||||||
} else {
|
|
||||||
alert('An error occured...');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}]);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
var menuService = angular.module('menuService', []);
|
|
||||||
|
|
||||||
menuService.factory('Menu', ['$location', function($location) {
|
|
||||||
|
|
||||||
var currentPage, currentRunId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
getCurrentPage: function() {
|
|
||||||
return currentPage;
|
|
||||||
},
|
|
||||||
setCurrentPage: function(page, runId) {
|
|
||||||
currentPage = page;
|
|
||||||
currentRunId = runId;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
changePage: function(page) {
|
|
||||||
switch (page) {
|
|
||||||
case 'index':
|
|
||||||
$location.path('/');
|
|
||||||
break;
|
|
||||||
case 'dashboard':
|
|
||||||
$location.path('/result/' + currentRunId);
|
|
||||||
break;
|
|
||||||
case 'timeline':
|
|
||||||
$location.path('/result/' + currentRunId + '/timeline');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.err('Undefined Menu.changePage() destination');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}]);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.about {
|
|
||||||
margin: 3em auto;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about p {
|
|
||||||
margin: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about a {
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
.testedUrl {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .globalScore {
|
|
||||||
display: table;
|
|
||||||
width: 60%;
|
|
||||||
margin: 3em auto;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: table-cell;
|
|
||||||
width: 50%;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.globalGrade {
|
|
||||||
margin: 0.5 auto;
|
|
||||||
width: 2.5em;
|
|
||||||
height: 2.5em;
|
|
||||||
line-height: 2.5em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
font-size: 3em;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.on100 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0.5em 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshotWrapper:hover {
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1.25em;
|
|
||||||
height: 1.25em;
|
|
||||||
top: 0.7em;
|
|
||||||
left: 1.55em;
|
|
||||||
font-size: 3em;
|
|
||||||
color: #FFF;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
text-align: center;
|
|
||||||
content: "+";
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .notations {
|
|
||||||
display: table;
|
|
||||||
width: 80%;
|
|
||||||
margin: 0 10% 1.5em;
|
|
||||||
border-spacing: 1em;
|
|
||||||
}
|
|
||||||
.summary .notations > div {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
.summary .notations > div > div {
|
|
||||||
display: table-cell;
|
|
||||||
height: 2.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .category {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria {
|
|
||||||
font-weight: normal;
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
.A, .B, .C, .D, .E, .F, .NA {
|
|
||||||
.summary .notations &.categoryScore {
|
|
||||||
width: 2.5em;
|
|
||||||
max-width: 2.5em;
|
|
||||||
min-width: 2.5em;
|
|
||||||
font-size: 2em;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.summary .notations .grade & {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
font-size: 1em;
|
|
||||||
color: transparent;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .notations .criteria .table {
|
|
||||||
width: 100%;
|
|
||||||
> div:hover > div {
|
|
||||||
background: #EBD8E2;
|
|
||||||
cursor: pointer;
|
|
||||||
&.info {
|
|
||||||
background: #FFF;
|
|
||||||
.icon-question {
|
|
||||||
color: #EBD8E2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .grade {
|
|
||||||
width: 10%;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .label {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .result {
|
|
||||||
width: 18%;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning {
|
|
||||||
color: #FF1919;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .info {
|
|
||||||
width: 2%;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
background: #FFF;
|
|
||||||
padding-left: 0.1em;
|
|
||||||
padding-right: 0.1em;
|
|
||||||
}
|
|
||||||
.summary .notations .criteria .icon-question {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .fromShare {
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .apiTip {
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-bottom: 4em;
|
|
||||||
color: #413;
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary .tweet {
|
|
||||||
.tweetText {
|
|
||||||
color: #413;
|
|
||||||
background: #F2F2F2;
|
|
||||||
border: none;
|
|
||||||
width: 25em;
|
|
||||||
padding: 0.4em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-shadow: 0.05em 0.1em 0 0 #999;
|
|
||||||
}
|
|
||||||
.tweetButton, .linkedinButton {
|
|
||||||
color: #413;
|
|
||||||
background: #F2F2F2;
|
|
||||||
margin-right: 0;
|
|
||||||
&:hover {
|
|
||||||
color: #F2F2F2;
|
|
||||||
background: #e74c3c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
@warning-font-family: "fontsmith-icons";
|
|
||||||
@warning-value: "\e000";
|
|
||||||
@warning: '"fontsmith-icons"' '"\\e000"';
|
|
||||||
@question-font-family: "fontsmith-icons";
|
|
||||||
@question-value: "\e001";
|
|
||||||
@question: '"fontsmith-icons"' '"\\e001"';
|
|
||||||
@lab-font-family: "fontsmith-icons";
|
|
||||||
@lab-value: "\e004";
|
|
||||||
@lab: '"fontsmith-icons"' '"\\e004"';
|
|
||||||
@list-font-family: "fontsmith-icons";
|
|
||||||
@list-value: "\e003";
|
|
||||||
@list: '"fontsmith-icons"' '"\\e003"';
|
|
||||||
@bars-font-family: "fontsmith-icons";
|
|
||||||
@bars-value: "\e005";
|
|
||||||
@bars: '"fontsmith-icons"' '"\\e005"';
|
|
||||||
@arrow-left3-font-family: "fontsmith-icons";
|
|
||||||
@arrow-left3-value: "\e006";
|
|
||||||
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
|
|
||||||
@loop-font-family: "fontsmith-icons";
|
|
||||||
@loop-value: "\e002";
|
|
||||||
@loop: '"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("/fonts/icons.woff") format("woff"),
|
|
||||||
;
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
.promess {
|
|
||||||
padding: 0em 2em 3em;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launchBtn {
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
&.disabled {
|
|
||||||
background: #deaca6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
|
||||||
display: table;
|
|
||||||
width: 50%;
|
|
||||||
margin: 8em auto 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #413;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 33.3%;
|
|
||||||
display: table-cell;
|
|
||||||
padding: 0 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,212 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
&.back, &.restart {
|
|
||||||
color: #413;
|
|
||||||
border-color: #413;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grade colors */
|
|
||||||
.A {
|
|
||||||
/* green */
|
|
||||||
background: #0C4;
|
|
||||||
}
|
|
||||||
.B {
|
|
||||||
/* green */
|
|
||||||
background: #CD0;
|
|
||||||
}
|
|
||||||
.C {
|
|
||||||
/* yellow */
|
|
||||||
background: #FD2;
|
|
||||||
}
|
|
||||||
.D {
|
|
||||||
/* orange */
|
|
||||||
background: #FA2;
|
|
||||||
}
|
|
||||||
.E {
|
|
||||||
/* red */
|
|
||||||
background: #F60;
|
|
||||||
}
|
|
||||||
.F {
|
|
||||||
/* red */
|
|
||||||
background: #F22;
|
|
||||||
}
|
|
||||||
.NA {
|
|
||||||
/* Non applicable */
|
|
||||||
background: #CCC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board {
|
|
||||||
margin-top: 2em;
|
|
||||||
padding: 1em;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backToDashboard {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: block;
|
|
||||||
margin-top: 4em;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.linkButton {
|
|
||||||
font-size: 1em;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
margin: 0.5em;
|
|
||||||
line-height: 2em;
|
|
||||||
border: 0 solid;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.screenshotWrapper.desktop {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
border: 0.2em solid #AAA;
|
|
||||||
background: #000;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-top-left-radius: 0.4em;
|
|
||||||
border-top-right-radius: 0.4em;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
position: absolute;
|
|
||||||
width: 15em;
|
|
||||||
height: 0.6em;
|
|
||||||
bottom: -0.75em;
|
|
||||||
left: -1em;
|
|
||||||
background: #CCC;
|
|
||||||
border-bottom-left-radius: 0.2em;
|
|
||||||
border-bottom-right-radius: 0.2em;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
position: absolute;
|
|
||||||
width: 0.4em;
|
|
||||||
height: 0.2em;
|
|
||||||
bottom: -0.55em;
|
|
||||||
left: 12.5em;
|
|
||||||
background: lime;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 12em;
|
|
||||||
height: 6.75em;
|
|
||||||
overflow: scroll;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshotImage {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.star {
|
|
||||||
font-weight: bold;
|
|
||||||
span {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
padding: 3em;
|
|
||||||
color: #413;
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.version {
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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-loop {
|
|
||||||
.icon(@loop);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.status {
|
|
||||||
margin-top: 2em;
|
|
||||||
font-size: 2.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusSubMessage {
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-bottom: 6em;
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
.rule.board {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .ruleTable {
|
|
||||||
display: table;
|
|
||||||
border-spacing: 1em;
|
|
||||||
width: 90%;
|
|
||||||
margin: 2em auto;
|
|
||||||
background: #f2f2f2;
|
|
||||||
border: 1px dashed #666;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
> div {
|
|
||||||
display: table-cell;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.left {
|
|
||||||
width: 33%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
width: 67%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .score {
|
|
||||||
font-size: 2.5em;
|
|
||||||
line-height: 2em;
|
|
||||||
height: 2em;
|
|
||||||
width: 2em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
margin: 0 auto 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .message {
|
|
||||||
width: 80%;
|
|
||||||
margin: 0 auto;
|
|
||||||
p {
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .message ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
.rule .message li:before {
|
|
||||||
content:'\25e6';
|
|
||||||
margin-right: 0.3em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
position: relative;
|
|
||||||
top: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .warning {
|
|
||||||
width: 90%;
|
|
||||||
margin: -1em auto 2em;
|
|
||||||
background: #FEE;
|
|
||||||
border: 1px dashed #e74c3c;
|
|
||||||
color: #e74c3c;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .offendersTable {
|
|
||||||
display: table;
|
|
||||||
border-spacing: 0 0.25em;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-width: 10%;
|
|
||||||
max-width: 90%;
|
|
||||||
> div {
|
|
||||||
display: table-row;
|
|
||||||
> div {
|
|
||||||
display: table-cell;
|
|
||||||
background: #f2f2f2;
|
|
||||||
padding: 0 1em;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-all;
|
|
||||||
&:hover {
|
|
||||||
background: #EBD8E2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule .notFound {
|
|
||||||
font-size: 1em;
|
|
||||||
h2 {
|
|
||||||
font-size: 3em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.offenders {
|
|
||||||
.offenderButton {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
background: #efe;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
margin: 0.2em 0;
|
|
||||||
border-radius: 0.4em;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&.opens {
|
|
||||||
padding-right: 0.75em;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
position: relative;
|
|
||||||
left: 0.5em;
|
|
||||||
content: '\25BC';
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
background: inherit;
|
|
||||||
border-bottom-left-radius: 0.4em;
|
|
||||||
border-bottom-right-radius: 0.4em;
|
|
||||||
border-top: 1px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domTree {
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin: 0.5em;
|
|
||||||
|
|
||||||
div {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.backtrace, .cssFileAndLine {
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.opens.mouseOver {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
background: #ffe0cc;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.offendersHtml {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domTree div {
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 1em;
|
|
||||||
|
|
||||||
span:only-child {
|
|
||||||
font-weight: bold;
|
|
||||||
span {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorPalette {
|
|
||||||
width: 30em;
|
|
||||||
border: 2px solid #000;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
/* Checkerboard background */
|
|
||||||
background-color: #ddd;
|
|
||||||
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
|
|
||||||
background-size:1em 1em;
|
|
||||||
background-position:0 0, 0.5em 0.5em;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: inline-block;
|
|
||||||
height: 2em;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 100%;
|
|
||||||
top: 100%;
|
|
||||||
background: #FFF;
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 2px solid #f1c40f;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
z-index: 3;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover div {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
background-color: inherit;
|
|
||||||
left: -0.2em;
|
|
||||||
top: -0.2em;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
border: 0.2em solid #f1c40f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.screenshot.board {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenshot .screenshotWrapper {
|
|
||||||
font-size: 2.08333333333333em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
@@ -1,271 +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;
|
|
||||||
|
|
||||||
.execution {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
margin: 1em auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
min-width: 30em;
|
|
||||||
width: 30%;
|
|
||||||
border: 1px dotted #aaa;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 .icon-warning {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.execution .icon-warning {
|
|
||||||
color: #e74c3c;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Yellow Lab Tools</title>
|
|
||||||
<base href="/">
|
|
||||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
|
||||||
<meta property="og:image" content="/img/logo-large.png" />
|
|
||||||
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
|
|
||||||
|
|
||||||
<!-- build:css /css/styles.css-->
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/main.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/queue.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/rule.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/about.css">
|
|
||||||
<!-- endbuild -->
|
|
||||||
|
|
||||||
<!-- build:js /js/all.js -->
|
|
||||||
<script src="/bower_components/angular/angular.min.js"></script>
|
|
||||||
<script src="/bower_components/angular-route/angular-route.min.js"></script>
|
|
||||||
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
|
|
||||||
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
|
|
||||||
<script src="/js/app.js"></script>
|
|
||||||
<script src="/js/controllers/indexCtrl.js"></script>
|
|
||||||
<script src="/js/controllers/dashboardCtrl.js"></script>
|
|
||||||
<script src="/js/controllers/queueCtrl.js"></script>
|
|
||||||
<script src="/js/controllers/ruleCtrl.js"></script>
|
|
||||||
<script src="/js/controllers/screenshotCtrl.js"></script>
|
|
||||||
<script src="/js/controllers/timelineCtrl.js"></script>
|
|
||||||
<script src="/js/models/resultsFactory.js"></script>
|
|
||||||
<script src="/js/models/runsFactory.js"></script>
|
|
||||||
<script src="/js/services/apiService.js"></script>
|
|
||||||
<script src="/js/services/menuService.js"></script>
|
|
||||||
<script src="/js/directives/gradeDirective.js"></script>
|
|
||||||
<script src="/js/directives/offendersDirectives.js"></script>
|
|
||||||
<!-- endbuild -->
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<body ng-app="YellowLabTools">
|
|
||||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
|
||||||
<div id="body" ng-view autoscroll="true"></div>
|
|
||||||
<div class="footer">
|
|
||||||
<a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!<br>
|
|
||||||
<span class="version">@@version</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(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');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<div class="about">
|
|
||||||
<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>. It lets you test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
|
|
||||||
|
|
||||||
<p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.</p>
|
|
||||||
|
|
||||||
<p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">a <span>★</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
|
|
||||||
|
|
||||||
<p><br><a href="/">Back to index</a></p>
|
|
||||||
</div>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
|
||||||
<div class="summary board">
|
|
||||||
|
|
||||||
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
|
|
||||||
<div>
|
|
||||||
<h2>Global score</h2>
|
|
||||||
<div class="globalScoreDisplay">
|
|
||||||
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
|
|
||||||
<div class="on100">{{globalScore}}/100</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="/result/{{result.runId}}/screenshot">
|
|
||||||
<div class="screenshotWrapper desktop">
|
|
||||||
<div>
|
|
||||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 ng-if="!error && !fromSocialShare">Score details</h2>
|
|
||||||
<div ng-if="!error && !fromSocialShare" class="notations">
|
|
||||||
<div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
|
|
||||||
<grade score="category.categoryScore" class="categoryScore"></grade>
|
|
||||||
<div class="category">{{category.label}}</div>
|
|
||||||
<div class="criteria">
|
|
||||||
<div class="table" title="Click to see details">
|
|
||||||
<div ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
|
|
||||||
ng-class="{'warning': rule.abnormal}" ng-click="showRulePage(ruleName)">
|
|
||||||
<div class="grade">
|
|
||||||
<grade score="rule.score"></grade>
|
|
||||||
</div>
|
|
||||||
<div class="label">{{rule.policy.label}}</div>
|
|
||||||
<div class="result">
|
|
||||||
{{rule.value}}
|
|
||||||
<span ng-if="rule.abnormal" class="icon-warning"></span>
|
|
||||||
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
|
|
||||||
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
|
|
||||||
</div>
|
|
||||||
<div class="info"><span class="icon-question"></span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
|
||||||
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tweet" ng-if="!error && !fromSocialShare">
|
|
||||||
<form ng-submit="sendTweet()">
|
|
||||||
<input type="text" class="tweetText" ng-model="tweetText">
|
|
||||||
<input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
|
|
||||||
<input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="fromShare" ng-if="!error && fromSocialShare">
|
|
||||||
<p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
|
|
||||||
<a class="linkButton" href="" ng-click="seeFullReport()">See the full report for this page</a>
|
|
||||||
<a class="linkButton" href="/">Test another webpage</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="error">
|
|
||||||
<h2>Run failed / Run not found</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<div class="offenderButton" ng-class="{opens: obj.tree}">
|
|
||||||
<span ng-if="obj.type == 'html' || obj.type == 'body' || obj.type == 'head' || obj.type == 'window' || obj.type == 'document' || obj.type == 'fragment'"><b>{{obj.type}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'domElement'">DOM element <b>{{obj.element}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'fragmentElement'">Fragment element <b>{{obj.element}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'createdElement'">Created element <b>{{obj.element}}</b></span>
|
|
||||||
<dom-tree ng-if="obj.tree" tree="obj.tree"></dom-tree>
|
|
||||||
</div>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
|
|
||||||
|
|
||||||
<form ng-submit="launchTest()" >
|
|
||||||
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
|
|
||||||
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="features">
|
|
||||||
<div>
|
|
||||||
<h3>Front-end quality</h3>
|
|
||||||
<p>Audits quality problems in the HTML, CSS and JS</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>WebPerf</h3>
|
|
||||||
<p>Checks if performance good practices are applied</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>JS Profiling</h3>
|
|
||||||
<p>Untangles the JavaScript spaghetti code</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<p ng-if="url">Tested url: <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
|
|
||||||
|
|
||||||
<div ng-if="!notFound && !connectionLost">
|
|
||||||
<div ng-if="status.statusCode == 'failed'">
|
|
||||||
<div class="status">Test failed</div>
|
|
||||||
<p class="statusSubMessage">{{status.error}}</p>
|
|
||||||
|
|
||||||
<a class="linkButton" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
|
|
||||||
<a class="linkButton" href="/">New test</a>
|
|
||||||
</div>
|
|
||||||
<div ng-if="status.statusCode == 'awaiting'">
|
|
||||||
<div class="status">
|
|
||||||
<ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
|
|
||||||
</ng-pluralize>
|
|
||||||
</div>
|
|
||||||
<p class="statusSubMessage">(auto-refresh activated)</p>
|
|
||||||
</div>
|
|
||||||
<div ng-if="status.statusCode == 'running'">
|
|
||||||
<div class="status">Test is running...</div>
|
|
||||||
<p class="statusSubMessage">(auto-refresh activated)</p>
|
|
||||||
</div>
|
|
||||||
<div ng-if="status.statusCode == 'complete'">
|
|
||||||
<div class="status">Test complete</div>
|
|
||||||
<p class="statusSubMessage">Opening results...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-if="notFound == true">
|
|
||||||
<div class="status">Error 404 (test not found)</div>
|
|
||||||
<p class="statusSubMessage">The server probably rebooted. We are very sorry about that, please try to launch the test again.</p>
|
|
||||||
|
|
||||||
<a class="linkButton" href="/">New test</a>
|
|
||||||
</div>
|
|
||||||
<div ng-if="connectionLost == true">
|
|
||||||
<div class="status">Connection lost with server</div>
|
|
||||||
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
|
|
||||||
</div>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<div>Tested url: <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
|
|
||||||
|
|
||||||
<div class="resultsMenu">
|
|
||||||
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
|
|
||||||
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
|
|
||||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
|
|
||||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
|
|
||||||
</div>
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
|
||||||
<div class="rule board">
|
|
||||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
|
||||||
|
|
||||||
<div ng-if="rule" class="ruleTable">
|
|
||||||
<div class="left">
|
|
||||||
<h2>{{rule.policy.label}}</h2>
|
|
||||||
<grade score="rule.score" class="score"></grade>
|
|
||||||
<div>{{rule.score}}/100</div>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<h3>Value: {{rule.value}}</h3>
|
|
||||||
<div ng-bind-html="rule.policy.message" class="message"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-if="rule.abnormal" class="warning">
|
|
||||||
<h3>Warning</h3>
|
|
||||||
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
|
|
||||||
</div>
|
|
||||||
<div class="offenders" ng-if="rule.policy.hasOffenders">
|
|
||||||
<h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
|
|
||||||
|
|
||||||
<div ng-if="rule.offendersObj.list" class="offendersTable">
|
|
||||||
<div ng-repeat="offender in rule.offendersObj.list track by $index">
|
|
||||||
<div ng-if="offender.parseError">
|
|
||||||
{{offender.parseError}}
|
|
||||||
</div>
|
|
||||||
<div ng-if="!offender.parseError">
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'DOMidDuplicated'">
|
|
||||||
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'DOMinserts'">
|
|
||||||
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
|
|
||||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'DOMqueriesAvoidable'">
|
|
||||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'eventsBound'">
|
|
||||||
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'jsErrors'">
|
|
||||||
<b>{{offender.error}}</b>
|
|
||||||
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
|
|
||||||
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
|
|
||||||
backtrace
|
|
||||||
<div class="backtrace">
|
|
||||||
<div ng-repeat="obj in offender.backtrace track by $index">
|
|
||||||
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
|
|
||||||
<url-link url="obj.file" max-length="60"></url-link>
|
|
||||||
line {{obj.line}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssParsingErrors'">
|
|
||||||
<b>{{offender.error}}</b>
|
|
||||||
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
|
|
||||||
<span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
|
|
||||||
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
|
|
||||||
<b ng-if="!offender.bolded">{{offender.css}}</b>
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssDuplicatedSelectors'">
|
|
||||||
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssDuplicatedProperties'">
|
|
||||||
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssEmptyRules'">
|
|
||||||
<b>{{offender.css}} { }</b>
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssExpressions'">
|
|
||||||
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssImportants'">
|
|
||||||
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssOldIEFixes'">
|
|
||||||
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
|
|
||||||
<span ng-bind-html="offender.bolded"></span>
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
|
|
||||||
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
|
|
||||||
<br>
|
|
||||||
<b>{{offender.message}}</b>
|
|
||||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
|
|
||||||
<url-link url="offender.file" max-length="100"></url-link>
|
|
||||||
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
|
|
||||||
<url-link url="offender" max-length="100"></url-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cachingTooShort'">
|
|
||||||
<url-link url="offender.file" max-length="100"></url-link>
|
|
||||||
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'domains'">
|
|
||||||
<b>{{offender.domain}}</b>
|
|
||||||
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
|
|
||||||
{{offender}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'DOMelementMaxDepth'">
|
|
||||||
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
|
||||||
<p>This is the colors palette, sized by total occurrences:</p>
|
|
||||||
<div class="colorPalette">
|
|
||||||
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div ng-if="!rule && rule !== null" class="notFound">
|
|
||||||
<h2>404</h2>
|
|
||||||
Rule "{{policyName}}"" not found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
|
||||||
</div>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
|
||||||
<div class="screenshot board">
|
|
||||||
<h2>Screenshot</h2>
|
|
||||||
|
|
||||||
<div class="screenshotWrapper desktop">
|
|
||||||
<div>
|
|
||||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>(scroll on the screenshot to see under the fold)</p>
|
|
||||||
|
|
||||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
|
||||||
</div>
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
|
||||||
<div class="execution board">
|
|
||||||
<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 < result.toolsResults.phantomas.metrics.domInteractive,
|
|
||||||
domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
|
|
||||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
|
|
||||||
domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
|
|
||||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
|
|
||||||
domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
|
|
||||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
|
|
||||||
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.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-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
|
|
||||||
ng-repeat="node in profilerData" ng-class="{
|
|
||||||
showingDetails: node.showDetails,
|
|
||||||
jsError: node.error,
|
|
||||||
windowPerformance: node.windowPerformance
|
|
||||||
}">
|
|
||||||
|
|
||||||
<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.warning && !node.error, 'icon-warning': node.warning || node.error}"
|
|
||||||
ng-click="onNodeDetailsClick(node)"
|
|
||||||
ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
|
|
||||||
|
|
||||||
<div class="detailsOverlay" ng-if="node.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.parsedBacktrace">
|
|
||||||
<h4>Backtrace</h4>
|
|
||||||
<div class="table">
|
|
||||||
<div ng-repeat="trace in node.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.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
|
|
||||||
<div>can't find any backtrace :/</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
|
||||||
</div>
|
|
||||||
@@ -2,6 +2,8 @@ var Q = require('q');
|
|||||||
|
|
||||||
var Runner = require('./runner');
|
var Runner = require('./runner');
|
||||||
|
|
||||||
|
var packageJson = require('../package.json');
|
||||||
|
|
||||||
|
|
||||||
var yellowLabTools = function(url, options) {
|
var yellowLabTools = function(url, options) {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -24,9 +26,13 @@ var yellowLabTools = function(url, options) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var runner = new Runner(params)
|
var runner = new Runner(params)
|
||||||
|
|
||||||
|
.progress(deferred.notify)
|
||||||
|
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
})
|
})
|
||||||
|
|
||||||
.fail(function(err) {
|
.fail(function(err) {
|
||||||
deferred.reject(err);
|
deferred.reject(err);
|
||||||
});
|
});
|
||||||
@@ -35,4 +41,5 @@ var yellowLabTools = function(url, options) {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = yellowLabTools;
|
module.exports = yellowLabTools;
|
||||||
|
module.exports.version = packageJson.version;
|
||||||
@@ -1,44 +1,58 @@
|
|||||||
{
|
{
|
||||||
"categories": {
|
"categories": {
|
||||||
|
"pageWeight": {
|
||||||
|
"label": "Page weight",
|
||||||
|
"policies": {
|
||||||
|
"totalWeight": 5,
|
||||||
|
"imageOptimization": 2,
|
||||||
|
"imagesTooLarge": 1,
|
||||||
|
"compression": 2,
|
||||||
|
"fileMinification": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"label": "Requests",
|
||||||
|
"policies": {
|
||||||
|
"totalRequests": 2,
|
||||||
|
"domains": 3,
|
||||||
|
"notFound": 2,
|
||||||
|
"identicalFiles": 2,
|
||||||
|
"emptyRequests": 3,
|
||||||
|
"lazyLoadableImagesBelowTheFold": 2,
|
||||||
|
"hiddenImages": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"domComplexity": {
|
"domComplexity": {
|
||||||
"label": "DOM complexity",
|
"label": "DOM complexity",
|
||||||
"policies": {
|
"policies": {
|
||||||
"DOMelementsCount": 1,
|
"DOMelementsCount": 3,
|
||||||
"DOMelementMaxDepth": 1,
|
"DOMelementMaxDepth": 1,
|
||||||
"iframesCount": 1,
|
"iframesCount": 1,
|
||||||
"DOMidDuplicated": 1
|
"DOMidDuplicated": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domManipulations": {
|
"javascriptComplexity": {
|
||||||
"label": "DOM manipulations",
|
"label": "JS complexity",
|
||||||
"policies": {
|
"policies": {
|
||||||
"DOMinserts": 2,
|
"scriptDuration": 4,
|
||||||
"DOMqueries": 1,
|
"DOMaccesses": 2,
|
||||||
"DOMqueriesWithoutResults": 2,
|
"eventsScrollBound": 1
|
||||||
"DOMqueriesAvoidable": 2,
|
|
||||||
"eventsBound": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"badJavascript": {
|
"badJavascript": {
|
||||||
"label": "Bad JavaScript",
|
"label": "Bad JS",
|
||||||
"policies": {
|
"policies": {
|
||||||
"jsErrors": 1,
|
"jsErrors": 1,
|
||||||
"documentWriteCalls": 2,
|
"documentWriteCalls": 2,
|
||||||
"consoleMessages": 0.5,
|
"synchronousXHR": 5,
|
||||||
"globalVariables": 0.5
|
"globalVariables": 0.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jQueryVersion": {
|
"jQuery": {
|
||||||
"label": "jQuery version",
|
"label": "jQuery",
|
||||||
"policies": {
|
"policies": {
|
||||||
"jQueryVersion": 5,
|
"jQueryVersion": 2,
|
||||||
"jQueryVersionsLoaded": 0.1
|
"jQueryVersionsLoaded": 2
|
||||||
}
|
|
||||||
},
|
|
||||||
"cssSyntaxError": {
|
|
||||||
"label": "CSS syntax errors",
|
|
||||||
"policies": {
|
|
||||||
"cssParsingErrors": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cssComplexity": {
|
"cssComplexity": {
|
||||||
@@ -46,70 +60,58 @@
|
|||||||
"policies": {
|
"policies": {
|
||||||
"cssRules": 2,
|
"cssRules": 2,
|
||||||
"cssComplexSelectors": 2,
|
"cssComplexSelectors": 2,
|
||||||
"cssComplexSelectorsByAttribute": 1.5,
|
"cssColors": 0.5,
|
||||||
"cssColors": 0.5
|
"similarColors": 0.5,
|
||||||
|
"cssBreakpoints": 0.5,
|
||||||
|
"cssMobileFirst": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"badCSS": {
|
"badCSS": {
|
||||||
"label": "Bad CSS",
|
"label": "Bad CSS",
|
||||||
"policies": {
|
"policies": {
|
||||||
"cssImports": 3,
|
"cssParsingErrors": 3,
|
||||||
|
"cssImports": 4,
|
||||||
"cssDuplicatedSelectors": 2,
|
"cssDuplicatedSelectors": 2,
|
||||||
"cssDuplicatedProperties": 1,
|
"cssDuplicatedProperties": 1,
|
||||||
"cssEmptyRules": 2,
|
"cssEmptyRules": 2,
|
||||||
"cssExpressions": 1,
|
"cssImportants": 2,
|
||||||
"cssImportants": 3,
|
|
||||||
"cssOldIEFixes": 1,
|
"cssOldIEFixes": 1,
|
||||||
"cssOldPropertyPrefixes": 1,
|
"cssOldPropertyPrefixes": 1,
|
||||||
"cssUniversalSelectors": 1,
|
|
||||||
"cssRedundantBodySelectors": 1,
|
"cssRedundantBodySelectors": 1,
|
||||||
"cssRedundantChildNodesSelectors": 1
|
"cssRedundantChildNodesSelectors": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requests": {
|
"fonts": {
|
||||||
"label": "Requests number",
|
"label": "Web fonts",
|
||||||
"policies": {
|
"policies": {
|
||||||
"requests": 5,
|
"fontsCount": 1,
|
||||||
"htmlCount": 0,
|
"heavyFonts": 0.5,
|
||||||
"jsCount": 1,
|
"unusedUnicodeRanges": 0.5,
|
||||||
"cssCount": 1,
|
"nonWoff2Fonts": 0.5
|
||||||
"imageCount": 0,
|
|
||||||
"webfontCount": 2,
|
|
||||||
"videoCount": 0,
|
|
||||||
"jsonCount": 0,
|
|
||||||
"otherCount": 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smallRequests": {
|
"serverConfig": {
|
||||||
"label": "Small requests",
|
"label": "Server config",
|
||||||
"policies": {
|
"policies": {
|
||||||
"smallJsFiles": 1,
|
"oldHttpProtocol": 2,
|
||||||
"smallCssFiles": 1,
|
"oldTlsProtocol": 2,
|
||||||
"smallImages": 1
|
"closedConnections": 2,
|
||||||
}
|
"cachingNotSpecified": 1,
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"label": "Network",
|
|
||||||
"policies": {
|
|
||||||
"notFound": 3,
|
|
||||||
"closedConnections": 3,
|
|
||||||
"multipleRequests": 3,
|
|
||||||
"cachingDisabled": 1,
|
"cachingDisabled": 1,
|
||||||
"cachingTooShort": 1,
|
"cachingTooShort": 1
|
||||||
"domains": 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalScore": {
|
"globalScore": {
|
||||||
"domComplexity": 1,
|
"pageWeight": 3,
|
||||||
"domManipulations": 2,
|
"requests": 2,
|
||||||
"badJavascript": 1,
|
"domComplexity": 2,
|
||||||
"jQueryVersion": 1,
|
"javascriptComplexity": 2,
|
||||||
"cssSyntaxError": 1,
|
"badJavascript": 2,
|
||||||
"cssComplexity": 1,
|
"jQuery": 0.5,
|
||||||
|
"cssComplexity": 0.5,
|
||||||
"badCSS": 1,
|
"badCSS": 1,
|
||||||
"requests": 3,
|
"fonts": 1,
|
||||||
"smallRequests": 1,
|
"serverConfig": 1
|
||||||
"network": 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,15 @@ var OffendersHelpers = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.domPathToDomElementObj = function(domPath) {
|
this.domPathToDomElementObj = function(domPath) {
|
||||||
|
|
||||||
|
if (typeof domPath === 'boolean') {
|
||||||
|
return {
|
||||||
|
// Not a normal element path
|
||||||
|
type: 'notAnElement',
|
||||||
|
element: domPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var domArray = this.domPathToArray(domPath);
|
var domArray = this.domPathToArray(domPath);
|
||||||
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
||||||
|
|
||||||
@@ -104,14 +113,17 @@ var OffendersHelpers = function() {
|
|||||||
if (traceArray) {
|
if (traceArray) {
|
||||||
var results = [];
|
var results = [];
|
||||||
var parts = null;
|
var parts = null;
|
||||||
|
var obj;
|
||||||
|
|
||||||
for (var i=0 ; i<traceArray.length ; i++) {
|
for (var i=0 ; i<traceArray.length ; i++) {
|
||||||
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
|
// Handle the new PhantomJS 2.x syntax
|
||||||
|
parts = /^\s*at( (\w+))? \(?([^ ]+):(\d+):(\d+)\)?$$/.exec(traceArray[i]);
|
||||||
|
|
||||||
if (parts) {
|
if (parts) {
|
||||||
var obj = {
|
obj = {
|
||||||
file: parts[3],
|
file: parts[3],
|
||||||
line: parseInt(parts[4], 10)
|
line: parseInt(parts[4], 10),
|
||||||
|
column: parseInt(parts[5], 10)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parts[2]) {
|
if (parts[2]) {
|
||||||
@@ -119,8 +131,7 @@ var OffendersHelpers = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
results.push(obj);
|
results.push(obj);
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@@ -142,24 +153,19 @@ var OffendersHelpers = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.cssOffenderPattern = function(offender) {
|
this.cssOffenderPattern = function(offender) {
|
||||||
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
|
// Used to work with strings
|
||||||
|
// As of Phantomas v2, offender is now in JSON format.
|
||||||
if (!parts) {
|
// Let's just adapt this for now and we'll see later if we remove completely this function
|
||||||
return {
|
return {
|
||||||
offender: offender
|
css: offender.value.message,
|
||||||
};
|
file: offender.url,
|
||||||
} else {
|
line: offender.value.position.start.line,
|
||||||
return {
|
column: offender.value.position.start.column
|
||||||
css: parts[1],
|
};
|
||||||
file: parts[2] || null,
|
|
||||||
line: parseInt(parts[3], 10),
|
|
||||||
column: parseInt(parts[4], 10)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fileWithSizePattern = function(fileWithSize) {
|
this.fileWithSizePattern = function(fileWithSize) {
|
||||||
var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize);
|
var parts = /^([^ ]*) \((\d+\.\d{2}|NaN) kB\)$/.exec(fileWithSize);
|
||||||
|
|
||||||
if (!parts) {
|
if (!parts) {
|
||||||
return {
|
return {
|
||||||
@@ -173,6 +179,34 @@ var OffendersHelpers = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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();
|
module.exports = new OffendersHelpers();
|
||||||
@@ -111,7 +111,8 @@ var RulesChecker = function() {
|
|||||||
if (rule) {
|
if (rule) {
|
||||||
rule.policy = {
|
rule.policy = {
|
||||||
label: policy.label,
|
label: policy.label,
|
||||||
message: policy.message
|
message: policy.message,
|
||||||
|
hasOffenders: policy.hasOffenders || false
|
||||||
};
|
};
|
||||||
|
|
||||||
results[metricName] = rule;
|
results[metricName] = rule;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ var Q = require('q');
|
|||||||
var debug = require('debug')('ylt:runner');
|
var debug = require('debug')('ylt:runner');
|
||||||
|
|
||||||
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
||||||
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
|
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 rulesChecker = require('./rulesChecker');
|
||||||
var scoreCalculator = require('./scoreCalculator');
|
var scoreCalculator = require('./scoreCalculator');
|
||||||
|
|
||||||
@@ -19,14 +22,67 @@ var Runner = function(params) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Execute Phantomas first
|
// Execute Phantomas first
|
||||||
phantomasWrapper.execute(data).then(function(phantomasResults) {
|
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;
|
data.toolsResults.phantomas = phantomasResults;
|
||||||
|
|
||||||
// Treat the JS Execution Tree from offenders
|
// Mix all DOM Access metrics together
|
||||||
data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
|
data = domAccessAgregator.agregate(data);
|
||||||
|
|
||||||
// Other tools go here
|
// 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.charactersCount) {
|
||||||
|
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
|
||||||
|
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
|
||||||
|
}
|
||||||
|
|
||||||
// Rules checker
|
// Rules checker
|
||||||
var policies = require('./metadata/policies');
|
var policies = require('./metadata/policies');
|
||||||
@@ -36,21 +92,42 @@ var Runner = function(params) {
|
|||||||
// Scores calculator
|
// Scores calculator
|
||||||
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
|
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
|
||||||
data.scoreProfiles = {
|
data.scoreProfiles = {
|
||||||
generic : scoreCalculator.calculate(data, scoreProfileGeneric)
|
generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
|
|
||||||
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
|
|
||||||
|
|
||||||
return data;
|
// 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');
|
||||||
|
|
||||||
}).then(function(data) {
|
|
||||||
|
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!
|
// Finished!
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
|
})
|
||||||
|
|
||||||
}).fail(function(err) {
|
.fail(function(err) {
|
||||||
debug('Run failed');
|
debug('Run failed');
|
||||||
debug(err);
|
debug(err);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator');
|
|||||||
var ScoreCalculator = function() {
|
var ScoreCalculator = function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
this.calculate = function(data, profile) {
|
this.calculate = function(data, profile, debugFlag) {
|
||||||
|
|
||||||
var results = {
|
var results = {
|
||||||
categories: {}
|
categories: {}
|
||||||
@@ -13,7 +13,9 @@ var ScoreCalculator = function() {
|
|||||||
var categoryName;
|
var categoryName;
|
||||||
var weight;
|
var weight;
|
||||||
|
|
||||||
debug('Starting calculating scores');
|
if (debugFlag) {
|
||||||
|
debug('Starting calculating scores');
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate categories
|
// Calculate categories
|
||||||
for (categoryName in profile.categories) {
|
for (categoryName in profile.categories) {
|
||||||
@@ -31,7 +33,7 @@ var ScoreCalculator = function() {
|
|||||||
if (data.rules[policyName]) {
|
if (data.rules[policyName]) {
|
||||||
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
|
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
|
||||||
categoryScore.push(policyScore, weight);
|
categoryScore.push(policyScore, weight);
|
||||||
} else {
|
} else if (debugFlag) {
|
||||||
debug('Warning: could not find rule %s', policyName);
|
debug('Warning: could not find rule %s', policyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +61,10 @@ var ScoreCalculator = function() {
|
|||||||
results.globalScore = Math.round(globalScore.getScore());
|
results.globalScore = Math.round(globalScore.getScore());
|
||||||
|
|
||||||
|
|
||||||
debug('Score calculation finished:');
|
if (debugFlag) {
|
||||||
debug(results);
|
debug('Score calculation finished:');
|
||||||
|
debug(results);
|
||||||
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
var debug = require('debug')('ylt:screenshotHandler');
|
|
||||||
var lwip = require('lwip');
|
|
||||||
var tmp = require('temporary');
|
|
||||||
var Q = require('q');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
|
|
||||||
var screenshotHandler = function() {
|
|
||||||
|
|
||||||
this.getScreenshotTempFile = function() {
|
|
||||||
|
|
||||||
var screenshotTmpFolder = new tmp.Dir();
|
|
||||||
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.jpg');
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
getTmpFolder: function() {
|
|
||||||
return screenshotTmpFolder;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTmpFilePath: function() {
|
|
||||||
return tmpFilePath;
|
|
||||||
},
|
|
||||||
|
|
||||||
toThumbnail: function(width) {
|
|
||||||
return that.optimize(tmpFilePath, width);
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteTmpFile: function() {
|
|
||||||
return that.deleteTmpFileAndFolder(tmpFilePath, screenshotTmpFolder);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.optimize = function(imagePath, width) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
debug('Starting screenshot transformation');
|
|
||||||
|
|
||||||
return this.openImage(imagePath)
|
|
||||||
|
|
||||||
.then(function(image) {
|
|
||||||
|
|
||||||
return that.resizeImage(image, width);
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(this.toBuffer);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.openImage = function(imagePath) {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
|
|
||||||
lwip.open(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.width();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.toBuffer = function(image) {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
|
|
||||||
image.toBuffer('jpg', {quality: 90}, 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.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
|
|
||||||
fs.unlink(tmpFilePath, function (err) {
|
|
||||||
if (err) {
|
|
||||||
debug('Screenshot file not found, could not be deleted. But it is not a problem.');
|
|
||||||
} else {
|
|
||||||
debug('Screenshot file deleted.');
|
|
||||||
}
|
|
||||||
|
|
||||||
screenshotTmpFolder.rmdir();
|
|
||||||
debug('Screenshot temp folder deleted');
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = new screenshotHandler();
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
var debug = require('debug')('ylt:server');
|
|
||||||
var Q = require('q');
|
|
||||||
|
|
||||||
var ylt = require('../../index');
|
|
||||||
var ScreenshotHandler = require('../../screenshotHandler');
|
|
||||||
var RunsQueue = require('../datastores/runsQueue');
|
|
||||||
var RunsDatastore = require('../datastores/runsDatastore');
|
|
||||||
var ResultsDatastore = require('../datastores/resultsDatastore');
|
|
||||||
|
|
||||||
|
|
||||||
var ApiController = function(app) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var queue = new RunsQueue();
|
|
||||||
var runsDatastore = new RunsDatastore();
|
|
||||||
var resultsDatastore = new ResultsDatastore();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create a new run
|
|
||||||
app.post('/api/runs', function(req, res) {
|
|
||||||
|
|
||||||
// Add http to the test URL
|
|
||||||
if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
|
|
||||||
req.body.url = 'http://' + req.body.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the test parameters and generate a random run ID
|
|
||||||
var run = {
|
|
||||||
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
|
|
||||||
params: {
|
|
||||||
url: req.body.url,
|
|
||||||
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
|
|
||||||
partialResult: req.body.partialResult || null,
|
|
||||||
screenshot: req.body.screenshot || false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a temporary folder to save the screenshot
|
|
||||||
var screenshot;
|
|
||||||
if (run.params.screenshot) {
|
|
||||||
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add test to the testQueue
|
|
||||||
debug('Adding test %s to the queue', run.runId);
|
|
||||||
var queuePromise = queue.push(run.runId);
|
|
||||||
|
|
||||||
|
|
||||||
// Save the run to the datastore
|
|
||||||
runsDatastore.add(run, queuePromise.startingPosition);
|
|
||||||
|
|
||||||
|
|
||||||
// Listening for position updates
|
|
||||||
queuePromise.progress(function(position) {
|
|
||||||
runsDatastore.updatePosition(run.runId, position);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's start the run
|
|
||||||
queuePromise.then(function() {
|
|
||||||
|
|
||||||
runsDatastore.updatePosition(run.runId, 0);
|
|
||||||
|
|
||||||
debug('Launching test %s on %s', run.runId, run.params.url);
|
|
||||||
|
|
||||||
var runOptions = {
|
|
||||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false
|
|
||||||
};
|
|
||||||
|
|
||||||
return ylt(run.params.url, runOptions);
|
|
||||||
|
|
||||||
})
|
|
||||||
// Phantomas completed, let's save the screenshot if any
|
|
||||||
.then(function(data) {
|
|
||||||
|
|
||||||
debug('Success');
|
|
||||||
data.runId = run.runId;
|
|
||||||
|
|
||||||
|
|
||||||
// Some conditional steps are made if there is a screenshot
|
|
||||||
var screenshotPromise = Q.resolve();
|
|
||||||
|
|
||||||
if (run.params.screenshot) {
|
|
||||||
|
|
||||||
// Replace the empty promise created earlier with Q.resolve()
|
|
||||||
screenshotPromise = screenshot.toThumbnail(400)
|
|
||||||
|
|
||||||
// Read screenshot
|
|
||||||
.then(function(screenshotBuffer) {
|
|
||||||
|
|
||||||
if (screenshotBuffer) {
|
|
||||||
debug('Image optimized');
|
|
||||||
data.screenshotBuffer = screenshotBuffer;
|
|
||||||
|
|
||||||
// Official path to get the image
|
|
||||||
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
// Delete screenshot temporary file
|
|
||||||
.then(screenshot.deleteTmpFile);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's continue
|
|
||||||
screenshotPromise
|
|
||||||
|
|
||||||
// Save results
|
|
||||||
.then(function() {
|
|
||||||
delete data.params.options.screenshot;
|
|
||||||
return resultsDatastore.saveResult(data);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mark as the run as complete and send the response if the request is still waiting
|
|
||||||
.then(function() {
|
|
||||||
|
|
||||||
debug('Result saved in datastore');
|
|
||||||
|
|
||||||
runsDatastore.markAsComplete(run.runId);
|
|
||||||
|
|
||||||
if (run.params.waitForResponse) {
|
|
||||||
|
|
||||||
// If the user only wants a portion of the result (partialResult option)
|
|
||||||
switch(run.params.partialResult) {
|
|
||||||
case 'generalScores':
|
|
||||||
res.redirect(302, '/api/results/' + run.runId + '/generalScores');
|
|
||||||
break;
|
|
||||||
case 'rules':
|
|
||||||
res.redirect(302, '/api/results/' + run.runId + '/rules');
|
|
||||||
break;
|
|
||||||
case 'javascriptExecutionTree':
|
|
||||||
res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
|
|
||||||
break;
|
|
||||||
case 'phantomas':
|
|
||||||
res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
res.redirect(302, '/api/results/' + run.runId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.fail(function(err) {
|
|
||||||
console.error('Test failed for URL: %s', run.params.url);
|
|
||||||
console.error(err.toString());
|
|
||||||
|
|
||||||
runsDatastore.markAsFailed(run.runId, err.toString());
|
|
||||||
|
|
||||||
res.status(500).send('An error occured');
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
.fail(function(err) {
|
|
||||||
|
|
||||||
console.error('Test failed for URL: %s', run.params.url);
|
|
||||||
console.error(err.toString());
|
|
||||||
|
|
||||||
runsDatastore.markAsFailed(run.runId, err.toString());
|
|
||||||
|
|
||||||
res.status(400).send('Bad request');
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
.finally(function() {
|
|
||||||
queue.remove(run.runId);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// The user doesn't want to wait for the response, sending the run ID only
|
|
||||||
if (!run.params.waitForResponse) {
|
|
||||||
console.log('Sending response without waiting.');
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.send(JSON.stringify({runId: run.runId}));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Retrive one run by id
|
|
||||||
app.get('/api/runs/:id', function(req, res) {
|
|
||||||
var runId = req.params.id;
|
|
||||||
|
|
||||||
var run = runsDatastore.get(runId);
|
|
||||||
|
|
||||||
if (run) {
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.send(JSON.stringify(run, null, 2));
|
|
||||||
} else {
|
|
||||||
res.status(404).send('Not found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieve the list of all runs
|
|
||||||
/*app.get('/api/runs', function(req, res) {
|
|
||||||
// NOT YET
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// Delete one run by id
|
|
||||||
/*app.delete('/api/runs/:id', function(req, res) {
|
|
||||||
deleteRun()
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// Delete all
|
|
||||||
/*app.delete('/api/runs', function(req, res) {
|
|
||||||
purgeRuns()
|
|
||||||
});
|
|
||||||
|
|
||||||
// List all
|
|
||||||
app.get('/api/runs', function(req, res) {
|
|
||||||
listRuns()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exists
|
|
||||||
app.head('/api/runs/:id', function(req, res) {
|
|
||||||
existsX();
|
|
||||||
// Returns 200 if the result exists or 404 if not
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Retrive one result by id
|
|
||||||
app.get('/api/results/:id', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieve one result and return only the generalScores part of the response
|
|
||||||
app.get('/api/results/:id/generalScores', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data.scoreProfiles.generic;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data.scoreProfiles[req.params.scoreProfile];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/results/:id/rules', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data.rules;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data.javascriptExecutionTree;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
|
|
||||||
getPartialResults(req.params.id, res, function(data) {
|
|
||||||
return data.toolsResults.phantomas;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function getPartialResults(runId, res, partialGetterFn) {
|
|
||||||
resultsDatastore.getResult(runId)
|
|
||||||
.then(function(data) {
|
|
||||||
var results = partialGetterFn(data);
|
|
||||||
|
|
||||||
if (typeof results === 'undefined') {
|
|
||||||
res.status(404).send('Not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.send(JSON.stringify(results, null, 2));
|
|
||||||
|
|
||||||
}).fail(function() {
|
|
||||||
res.status(404).send('Not found');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrive one result by id
|
|
||||||
app.get('/api/results/:id/screenshot.jpg', function(req, res) {
|
|
||||||
var runId = req.params.id;
|
|
||||||
|
|
||||||
resultsDatastore.getScreenshot(runId)
|
|
||||||
.then(function(screenshotBuffer) {
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'image/jpeg');
|
|
||||||
res.send(screenshotBuffer);
|
|
||||||
|
|
||||||
}).fail(function() {
|
|
||||||
res.status(404).send('Not found');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = ApiController;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
var path = require('path');
|
|
||||||
var express = require('express');
|
|
||||||
|
|
||||||
var FrontController = function(app) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
|
|
||||||
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
|
|
||||||
|
|
||||||
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
|
|
||||||
routes.forEach(function(route) {
|
|
||||||
app.get(route, function(req, res) {
|
|
||||||
res.setHeader('Cache-Control', 'public, max-age=20');
|
|
||||||
res.sendFile(path.join(__dirname, assetsPath, 'main.html'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
|
|
||||||
app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
|
|
||||||
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
|
|
||||||
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
|
|
||||||
app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
|
|
||||||
app.use('/bower_components', express.static(path.join(__dirname, '../../../bower_components'), { maxAge: cacheDuration }));
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = FrontController;
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
var fs = require('fs');
|
|
||||||
var rimraf = require('rimraf');
|
|
||||||
var path = require('path');
|
|
||||||
var Q = require('q');
|
|
||||||
var debug = require('debug')('ylt:resultsDatastore');
|
|
||||||
|
|
||||||
|
|
||||||
function ResultsDatastore() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var resultFileName = 'results.json';
|
|
||||||
var resultScreenshotName = 'screenshot.jpg';
|
|
||||||
var resultsFolderName = 'results';
|
|
||||||
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
|
|
||||||
|
|
||||||
|
|
||||||
this.saveResult = function(testResults) {
|
|
||||||
|
|
||||||
return createResultFolder(testResults.runId)
|
|
||||||
|
|
||||||
.then(function() {
|
|
||||||
return saveScreenshotIfExists(testResults);
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(function() {
|
|
||||||
|
|
||||||
debug('Saving results to disk...');
|
|
||||||
|
|
||||||
var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
|
|
||||||
debug('Destination file is %s', resultFilePath);
|
|
||||||
|
|
||||||
return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.getResult = function(runId) {
|
|
||||||
|
|
||||||
var resultFilePath = path.join(resultsDir, runId, resultFileName);
|
|
||||||
|
|
||||||
debug('Reading results (runID = %s) from disk...', runId);
|
|
||||||
|
|
||||||
return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
|
|
||||||
return JSON.parse(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.deleteResult = function(runId) {
|
|
||||||
var folder = path.join(resultsDir, runId);
|
|
||||||
|
|
||||||
debug('Deleting results (runID = %s) from disk...', runId);
|
|
||||||
|
|
||||||
return Q.nfcall(rimraf, folder);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// The folder /results/folderName/
|
|
||||||
function createResultFolder(folderName) {
|
|
||||||
var folder = path.join(resultsDir, folderName);
|
|
||||||
|
|
||||||
debug('Creating the folder %s', folderName);
|
|
||||||
|
|
||||||
return createGlobalFolder().then(function() {
|
|
||||||
return Q.nfcall(fs.mkdir, folder);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// The folder /results/
|
|
||||||
function createGlobalFolder() {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
|
|
||||||
// Create the results folder if it doesn't exist
|
|
||||||
fs.exists(resultsDir, function(exists) {
|
|
||||||
if (exists) {
|
|
||||||
deferred.resolve();
|
|
||||||
} else {
|
|
||||||
debug('Creating the global results folder', resultsDir);
|
|
||||||
fs.mkdir(resultsDir, function(err) {
|
|
||||||
if (err) {
|
|
||||||
deferred.reject(err);
|
|
||||||
} else {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
|
|
||||||
function saveScreenshotIfExists(testResults) {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
|
|
||||||
if (testResults.screenshotBuffer) {
|
|
||||||
|
|
||||||
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
|
|
||||||
fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
|
|
||||||
|
|
||||||
delete testResults.screenshotBuffer;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getScreenshot = function(runId) {
|
|
||||||
|
|
||||||
var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
|
|
||||||
|
|
||||||
debug('Getting screenshot (runID = %s) from disk...', runId);
|
|
||||||
|
|
||||||
return Q.nfcall(fs.readFile, screenshotFilePath);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ResultsDatastore;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
function RunsDatastore() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// NOT PERSISTING RUNS
|
|
||||||
// For the moment, maybe one day
|
|
||||||
var runs = {};
|
|
||||||
|
|
||||||
var STATUS_AWAITING = 'awaiting';
|
|
||||||
var STATUS_RUNNING = 'running';
|
|
||||||
var STATUS_COMPLETE = 'complete';
|
|
||||||
var STATUS_FAILED = 'failed';
|
|
||||||
|
|
||||||
|
|
||||||
this.add = function(run, position) {
|
|
||||||
runs[run.runId] = run;
|
|
||||||
this.updatePosition(run.runId, position);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.get = function(runId) {
|
|
||||||
return runs[runId];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.updatePosition = function(runId, position) {
|
|
||||||
var run = runs[runId];
|
|
||||||
|
|
||||||
if (position > 0) {
|
|
||||||
run.status = {
|
|
||||||
statusCode: STATUS_AWAITING,
|
|
||||||
position: position
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
run.status = {
|
|
||||||
statusCode: STATUS_RUNNING
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
runs[runId] = run;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.markAsComplete = function(runId) {
|
|
||||||
var run = runs[runId];
|
|
||||||
|
|
||||||
run.status = {
|
|
||||||
statusCode: STATUS_COMPLETE
|
|
||||||
};
|
|
||||||
|
|
||||||
runs[runId] = run;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.markAsFailed = function(runId, err) {
|
|
||||||
var run = runs[runId];
|
|
||||||
|
|
||||||
var errorMessage;
|
|
||||||
switch(err) {
|
|
||||||
case '252':
|
|
||||||
errorMessage = "Error 252: page timeout in Phantomas";
|
|
||||||
break;
|
|
||||||
case '253':
|
|
||||||
errorMessage = "Error 253: Phantomas config error";
|
|
||||||
break;
|
|
||||||
case '254':
|
|
||||||
errorMessage = "Error 254: page loading failed in PhantomJS";
|
|
||||||
break;
|
|
||||||
case '255':
|
|
||||||
errorMessage = "Error 255: Phantomas error";
|
|
||||||
break;
|
|
||||||
case '1001':
|
|
||||||
errorMessage = "Error 1001: JavaScript profiling failed";
|
|
||||||
break;
|
|
||||||
case '1002':
|
|
||||||
errorMessage = "Error 1002: missing Phantomas metrics";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
errorMessage = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
run.status = {
|
|
||||||
statusCode: STATUS_FAILED,
|
|
||||||
error: errorMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
runs[runId] = run;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.delete = function(runId) {
|
|
||||||
delete runs[runId];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.list = function() {
|
|
||||||
var runsArray = [];
|
|
||||||
Object.keys(runs).forEach(function(key) {
|
|
||||||
runsArray.push(runs[key]);
|
|
||||||
});
|
|
||||||
return runsArray;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RunsDatastore;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
var Q = require('q');
|
|
||||||
var debug = require('debug')('ylt:runsQueue');
|
|
||||||
|
|
||||||
|
|
||||||
function RunsQueue() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var queue = [];
|
|
||||||
|
|
||||||
|
|
||||||
this.push = function(runId) {
|
|
||||||
var deferred = Q.defer();
|
|
||||||
var startingPosition = queue.length;
|
|
||||||
|
|
||||||
debug('Adding run %s to the queue, position is %d', runId, startingPosition);
|
|
||||||
|
|
||||||
if (startingPosition === 0) {
|
|
||||||
|
|
||||||
// The queue is empty, let's run immediatly
|
|
||||||
queue.push({
|
|
||||||
runId: runId
|
|
||||||
});
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
queue.push({
|
|
||||||
runId: runId,
|
|
||||||
positionChangedCallback: function(position) {
|
|
||||||
deferred.notify(position);
|
|
||||||
},
|
|
||||||
itIsTimeCallback: function() {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var promise = deferred.promise;
|
|
||||||
promise.startingPosition = startingPosition;
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.getPosition = function(runId) {
|
|
||||||
// Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before)
|
|
||||||
var position = -1;
|
|
||||||
|
|
||||||
queue.some(function(run, index) {
|
|
||||||
if (run.runId === runId) {
|
|
||||||
position = index;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return position;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.remove = function(runId) {
|
|
||||||
var position = this.getPosition(runId);
|
|
||||||
if (position >= 0) {
|
|
||||||
queue.splice(position, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update other runs' positions
|
|
||||||
queue.forEach(function(run, index) {
|
|
||||||
if (index === 0 && run.itIsTimeCallback) {
|
|
||||||
run.itIsTimeCallback();
|
|
||||||
} else if (index > 0 && run.positionChangedCallback) {
|
|
||||||
run.positionChangedCallback(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.length = function() {
|
|
||||||
return queue.length;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RunsQueue;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
var config = require('../../../server_config/settings.json');
|
|
||||||
|
|
||||||
var debug = require('debug')('apiLimitsMiddleware');
|
|
||||||
|
|
||||||
|
|
||||||
var apiLimitsMiddleware = function(req, res, next) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
|
|
||||||
|
|
||||||
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
|
|
||||||
|
|
||||||
|
|
||||||
if (req.path === '/api/runs') {
|
|
||||||
|
|
||||||
if (!runsTable.accepts(req.connection.remoteAddress)) {
|
|
||||||
// Sorry :/
|
|
||||||
debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
|
|
||||||
res.status(429).send('Too many requests');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!callsTable.accepts(req.connection.remoteAddress)) {
|
|
||||||
// Sorry :/
|
|
||||||
debug('Too many API requests from IP address %s', req.connection.remoteAddress);
|
|
||||||
res.status(429).send('Too many requests');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Not blocked by the API limits');
|
|
||||||
// It's ok for the moment
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var RecordTable = function(maxPerDay) {
|
|
||||||
var table = {};
|
|
||||||
|
|
||||||
// Check if the user overpassed the limit and save its visit
|
|
||||||
this.accepts = function(ipAddress) {
|
|
||||||
if (table[ipAddress]) {
|
|
||||||
|
|
||||||
this.cleanEntry(ipAddress);
|
|
||||||
|
|
||||||
debug('%d visits in the last 24 hours', table[ipAddress].length);
|
|
||||||
|
|
||||||
if (table[ipAddress].length >= maxPerDay) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
table[ipAddress].push(Date.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
table[ipAddress] = [];
|
|
||||||
table[ipAddress].push(Date.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clean the table for this guy
|
|
||||||
this.cleanEntry = function(ipAddress) {
|
|
||||||
table[ipAddress] = table[ipAddress].filter(function(date) {
|
|
||||||
return date > Date.now() - 1000*60*60*24;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clean the entire table once in a while
|
|
||||||
this.removeOld = function() {
|
|
||||||
for (var ipAddress in table) {
|
|
||||||
this.cleanEntry(ipAddress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// Init the records tables
|
|
||||||
var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
|
|
||||||
var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
|
|
||||||
|
|
||||||
module.exports = apiLimitsMiddleware;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
var config = require('../../../server_config/settings.json');
|
|
||||||
|
|
||||||
var debug = require('debug')('authMiddleware');
|
|
||||||
|
|
||||||
|
|
||||||
var authMiddleware = function(req, res, next) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (req.path.indexOf('/api/') === 0) {
|
|
||||||
|
|
||||||
|
|
||||||
if (req.headers && req.headers['x-api-key']) {
|
|
||||||
|
|
||||||
// Test if it's an authorized key
|
|
||||||
if (isApiKeyValid(req.headers['x-api-key'])) {
|
|
||||||
|
|
||||||
// Come in!
|
|
||||||
debug('Authorized key: %s', req.headers['x-api-key']);
|
|
||||||
res.locals.hasApiKey = true;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Sorry :/
|
|
||||||
debug('Unauthorized key %s', req.headers['x-api-key']);
|
|
||||||
res.status(401).send('Unauthorized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug('No authorization key');
|
|
||||||
// It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function isApiKeyValid(apiKey) {
|
|
||||||
return (config.authorizedKeys[apiKey]) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = authMiddleware;
|
|
||||||
@@ -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();
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
var debug = require('debug')('ylt:jsExecutionTransformer');
|
|
||||||
|
|
||||||
var jsExecutionTransformer = function() {
|
|
||||||
|
|
||||||
this.transform = function(data) {
|
|
||||||
var javascriptExecutionTree = {};
|
|
||||||
|
|
||||||
debug('Starting JS execution transformation');
|
|
||||||
|
|
||||||
try {
|
|
||||||
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
|
|
||||||
|
|
||||||
if (javascriptExecutionTree.children) {
|
|
||||||
javascriptExecutionTree.children.forEach(function(node) {
|
|
||||||
|
|
||||||
// Mark abnormal things with a warning flag
|
|
||||||
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
|
|
||||||
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
|
|
||||||
node.data.resultsNumber === 0 ||
|
|
||||||
contextLenght === 0) {
|
|
||||||
node.warning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark errors with an error flag
|
|
||||||
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
|
|
||||||
node.error = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark a performance flag
|
|
||||||
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
|
|
||||||
node.windowPerformance = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
|
|
||||||
switch(node.data.type) {
|
|
||||||
case 'domInteractive':
|
|
||||||
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
|
|
||||||
break;
|
|
||||||
case 'domContentLoaded':
|
|
||||||
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
|
|
||||||
break;
|
|
||||||
case 'domContentLoadedEnd':
|
|
||||||
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
|
|
||||||
break;
|
|
||||||
case 'domComplete':
|
|
||||||
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('JS execution transformation complete');
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return javascriptExecutionTree;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = new jsExecutionTransformer();
|
|
||||||
@@ -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();
|
||||||
@@ -1,205 +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.2';
|
|
||||||
|
|
||||||
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 YLT spy to "' + fn + '" function...');
|
|
||||||
|
|
||||||
obj[fn] = function() {
|
|
||||||
var result;
|
|
||||||
var err;
|
|
||||||
|
|
||||||
// Before
|
|
||||||
if (enabled) {
|
|
||||||
callbackBefore.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
try {
|
|
||||||
|
|
||||||
result = origFn.apply(this, arguments);
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
// Catching the err for the moment, because we need to make sure the callbackAfter function is called.
|
|
||||||
|
|
||||||
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
|
|
||||||
phantomas.log(arguments);
|
|
||||||
|
|
||||||
err = e;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
|
|
||||||
// After
|
|
||||||
if (enabled && callbackAfter) {
|
|
||||||
callbackAfter.call(this, result, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
phantomas.log('Re-throwing the error');
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Analyzes HTTP caching headers
|
|
||||||
*
|
|
||||||
* @see https://developers.google.com/speed/docs/best-practices/caching
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.version = '0.2.a';
|
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var cacheControlRegExp = /max-age=(\d+)/;
|
|
||||||
|
|
||||||
function getCachingTime(url, headers) {
|
|
||||||
// false means "no caching"
|
|
||||||
var ttl = false,
|
|
||||||
headerName,
|
|
||||||
now = new Date(),
|
|
||||||
headerDate;
|
|
||||||
|
|
||||||
for (headerName in headers) {
|
|
||||||
var value = headers[headerName];
|
|
||||||
|
|
||||||
switch (headerName.toLowerCase()) {
|
|
||||||
// parse max-age=...
|
|
||||||
//
|
|
||||||
// max-age=2592000
|
|
||||||
// public, max-age=300, must-revalidate
|
|
||||||
case 'cache-control':
|
|
||||||
var matches = value.match(cacheControlRegExp);
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
ttl = parseInt(matches[1], 10);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// catch Expires and Pragma headers
|
|
||||||
case 'expires':
|
|
||||||
case 'pragma':
|
|
||||||
// and Varnish specific headers
|
|
||||||
case 'x-pass-expires':
|
|
||||||
case 'x-pass-cache-control':
|
|
||||||
phantomas.incrMetric('oldCachingHeaders'); // @desc number of responses with old, HTTP 1.0 caching headers (Expires and Pragma)
|
|
||||||
phantomas.addOffender('oldCachingHeaders', url + ' - ' + headerName + ': ' + value);
|
|
||||||
headerDate = Date.parse(value);
|
|
||||||
if (headerDate) ttl = Math.round((headerDate - now) / 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log(JSON.stringify(headers)); console.log("TTL: " + ttl + ' s');
|
|
||||||
|
|
||||||
return ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
phantomas.setMetric('cachingNotSpecified'); // @desc number of responses with no caching header sent (no Cache-Control header)
|
|
||||||
phantomas.setMetric('cachingTooShort'); // @desc number of responses with too short (less than a week) caching time
|
|
||||||
phantomas.setMetric('cachingDisabled'); // @desc number of responses with caching disabled (max-age=0)
|
|
||||||
|
|
||||||
phantomas.setMetric('oldCachingHeaders');
|
|
||||||
|
|
||||||
phantomas.on('recv', function(entry, res) {
|
|
||||||
var ttl = getCachingTime(entry.url, entry.headers);
|
|
||||||
|
|
||||||
// static assets
|
|
||||||
if (entry.isImage || entry.isJS || entry.isCSS) {
|
|
||||||
if (ttl === false) {
|
|
||||||
phantomas.incrMetric('cachingNotSpecified');
|
|
||||||
phantomas.addOffender('cachingNotSpecified', entry.url);
|
|
||||||
} else if (ttl <= 0) {
|
|
||||||
phantomas.incrMetric('cachingDisabled');
|
|
||||||
phantomas.addOffender('cachingDisabled', entry.url);
|
|
||||||
} else if (ttl < 7 * 86400) {
|
|
||||||
phantomas.incrMetric('cachingTooShort');
|
|
||||||
phantomas.addOffender('cachingTooShort', entry.url + ' cached for ' + ttl + ' s');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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.");
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
/**
|
|
||||||
* Analyzes DOM queries done via native DOM methods
|
|
||||||
*/
|
|
||||||
/* global Element: true, Document: true, Node: true, window: true */
|
|
||||||
|
|
||||||
exports.version = '0.10.a';
|
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
|
|
||||||
phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @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, hasNoResults) {
|
|
||||||
phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
|
|
||||||
}
|
|
||||||
|
|
||||||
phantomas.spy(Document.prototype, 'getElementById', function(id) {
|
|
||||||
phantomas.incrMetric('DOMqueriesById');
|
|
||||||
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
|
|
||||||
|
|
||||||
phantomas.enterContext({
|
|
||||||
type: 'getElementById',
|
|
||||||
callDetails: {
|
|
||||||
context: {
|
|
||||||
domElement: '#document'
|
|
||||||
},
|
|
||||||
arguments: ['#' + id]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
|
|
||||||
}, function(result, args) {
|
|
||||||
var id = args[0];
|
|
||||||
|
|
||||||
querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
|
|
||||||
|
|
||||||
var moreData = {
|
|
||||||
resultsNumber : (result === null) ? 0 : 1
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
|
|
||||||
phantomas.enterContext({
|
|
||||||
type: 'getElementsByClassName',
|
|
||||||
callDetails: {
|
|
||||||
context: {
|
|
||||||
domElement: context
|
|
||||||
},
|
|
||||||
arguments: ['.' + className]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectorClassNameAfter(result, args) {
|
|
||||||
/*jshint validthis: true */
|
|
||||||
|
|
||||||
var className = args[0];
|
|
||||||
var context = phantomas.getDOMPath(this);
|
|
||||||
|
|
||||||
querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
phantomas.enterContext({
|
|
||||||
type: 'getElementsByTagName',
|
|
||||||
callDetails: {
|
|
||||||
context: {
|
|
||||||
domElement: context
|
|
||||||
},
|
|
||||||
arguments: [tagName]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectorTagNameSpyAfter(result, args) {
|
|
||||||
/*jshint validthis: true */
|
|
||||||
|
|
||||||
var tagName = args[0];
|
|
||||||
var context = phantomas.getDOMPath(this);
|
|
||||||
|
|
||||||
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, args) {
|
|
||||||
/*jshint validthis: true */
|
|
||||||
|
|
||||||
var selector = args[0];
|
|
||||||
var context = phantomas.getDOMPath(this);
|
|
||||||
|
|
||||||
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
|
|
||||||
|
|
||||||
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, args) {
|
|
||||||
/*jshint validthis: true */
|
|
||||||
|
|
||||||
var selector = args[0];
|
|
||||||
var context = phantomas.getDOMPath(this);
|
|
||||||
|
|
||||||
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// report DOM queries that return no results (issue #420)
|
|
||||||
phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
|
|
||||||
// ignore DOM queries within DOM fragments (used internally by jQuery)
|
|
||||||
if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNoResults === true) {
|
|
||||||
phantomas.incrMetric('DOMqueriesWithoutResults');
|
|
||||||
phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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('../../util/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,318 +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 = '1.0.a';
|
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
|
|
||||||
phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
|
|
||||||
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
|
|
||||||
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
|
|
||||||
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
|
|
||||||
//phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
|
|
||||||
|
|
||||||
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',
|
|
||||||
'bind',
|
|
||||||
'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;
|
|
||||||
var oldJQuery;
|
|
||||||
|
|
||||||
phantomas.spyGlobalVar('jQuery', function(jQuery) {
|
|
||||||
var version;
|
|
||||||
|
|
||||||
if (!jQuery || !jQuery.fn) {
|
|
||||||
phantomas.log('jQuery: unable to detect version!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
|
|
||||||
// when it's actually only restored via $.noConflict(true) - see comments in #435
|
|
||||||
if (jQuery.__phantomas === true) {
|
|
||||||
phantomas.log('jQuery: this instance has already been seen by phantomas');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jQuery.__phantomas = true;
|
|
||||||
|
|
||||||
// report the version of jQuery
|
|
||||||
version = jQuery.fn.jquery;
|
|
||||||
phantomas.emit('jQueryLoaded', version);
|
|
||||||
|
|
||||||
phantomas.pushContext({
|
|
||||||
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
|
|
||||||
callDetails: {
|
|
||||||
arguments: ['version ' + version]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
oldJQuery = version;
|
|
||||||
|
|
||||||
// jQuery.ready.promise
|
|
||||||
// works for jQuery 1.8.0+ (released Aug 09 2012)
|
|
||||||
phantomas.spy(jQuery.ready, 'promise', function(func) {
|
|
||||||
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
|
|
||||||
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
|
|
||||||
|
|
||||||
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(jQuery, 'find', function(selector, context) {
|
|
||||||
phantomas.incrMetric('jQuerySizzleCalls');
|
|
||||||
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (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)!');
|
|
||||||
|
|
||||||
/*if (!jQuery.event) {
|
|
||||||
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
|
|
||||||
var path = phantomas.getDOMPath(elem),
|
|
||||||
type = ev.type || ev;
|
|
||||||
|
|
||||||
phantomas.log('Event: triggered "%s" on "%s"', type, path);
|
|
||||||
|
|
||||||
phantomas.incrMetric('jQueryEventTriggers');
|
|
||||||
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// jQuery events bound to window' onLoad event (#451)
|
|
||||||
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
|
|
||||||
if ((eventName === 'load') && (this[0] === window)) {
|
|
||||||
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
|
|
||||||
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
|
|
||||||
|
|
||||||
phantomas.pushContext({
|
|
||||||
type: 'jQuery - windowOnLoad',
|
|
||||||
callDetails: {
|
|
||||||
arguments: [func]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add spys on many jQuery functions
|
|
||||||
jQueryFunctions.forEach(function(functionName) {
|
|
||||||
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
|
|
||||||
|
|
||||||
phantomas.spy(jQuery.fn, 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.__phantomas);
|
|
||||||
}, jQueryFunctions);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// store the last resource that was received
|
|
||||||
// try to report where given jQuery version was loaded from
|
|
||||||
phantomas.on('recv', function(entry) {
|
|
||||||
if (entry.isJS) {
|
|
||||||
lastUrl = entry.url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
phantomas.on('jQueryLoaded', function(version) {
|
|
||||||
phantomas.log('jQuery: loaded v' + version);
|
|
||||||
phantomas.setMetric('jQueryVersion', version);
|
|
||||||
|
|
||||||
// report multiple jQuery "instances" (issue #435)
|
|
||||||
phantomas.incrMetric('jQueryVersionsLoaded');
|
|
||||||
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
|
|
||||||
|
|
||||||
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* Reports the use of functions known to be serious performance bottlenecks in JS
|
|
||||||
*
|
|
||||||
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
|
|
||||||
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
|
|
||||||
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
|
|
||||||
*/
|
|
||||||
/* global document: true, window: true */
|
|
||||||
|
|
||||||
exports.version = '0.1.a';
|
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln
|
|
||||||
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
|
|
||||||
|
|
||||||
phantomas.once('init', function() {
|
|
||||||
phantomas.evaluate(function() {
|
|
||||||
(function(phantomas) {
|
|
||||||
function report(msg, caller, backtrace, metric) {
|
|
||||||
phantomas.log(msg + ': from ' + caller + '!');
|
|
||||||
phantomas.log('Backtrace: ' + backtrace);
|
|
||||||
phantomas.incrMetric(metric);
|
|
||||||
}
|
|
||||||
|
|
||||||
// spy calls to eval()
|
|
||||||
/*phantomas.spy(window, 'eval', function(code) {
|
|
||||||
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
|
||||||
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// spy calls to setTimeout / setInterval with string passed instead of a function
|
|
||||||
/*phantomas.spy(window, 'setTimeout', function(fn, interval) {
|
|
||||||
if (typeof fn !== 'string') return;
|
|
||||||
|
|
||||||
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
|
||||||
});*/
|
|
||||||
|
|
||||||
/*phantomas.spy(window, 'setInterval', function(fn, interval) {
|
|
||||||
if (typeof fn !== 'string') return;
|
|
||||||
|
|
||||||
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// spy document.write(ln)
|
|
||||||
phantomas.spy(document, 'write', function(arg) {
|
|
||||||
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
|
||||||
});
|
|
||||||
|
|
||||||
phantomas.spy(document, 'writeln', function(arg) {
|
|
||||||
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
|
||||||
});
|
|
||||||
})(window.__phantomas);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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,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,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,6 +1,5 @@
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
var Q = require('q');
|
var Q = require('q');
|
||||||
var ps = require('ps-node');
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var debug = require('debug')('ylt:phantomaswrapper');
|
var debug = require('debug')('ylt:phantomaswrapper');
|
||||||
var phantomas = require('phantomas');
|
var phantomas = require('phantomas');
|
||||||
@@ -11,131 +10,85 @@ var PhantomasWrapper = function() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the phantomas launcher. It merges user chosen options into the default options
|
* 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?
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
this.execute = function(data) {
|
this.execute = function(data) {
|
||||||
|
|
||||||
var deferred = Q.defer();
|
var deferred = Q.defer();
|
||||||
var task = data.params;
|
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 = {
|
var options = {
|
||||||
|
|
||||||
// Cusomizable options
|
// Cusomizable options
|
||||||
'timeout': task.options.timeout || 60,
|
'timeout': task.options.timeout || 120,
|
||||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
'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,
|
||||||
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
|
'tablet': (task.options.device === 'tablet'),
|
||||||
|
'phone': (task.options.device === 'phone'),
|
||||||
'screenshot': task.options.screenshot || false,
|
'screenshot': task.options.screenshot || false,
|
||||||
'viewport': '1366x768',
|
'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,
|
||||||
|
|
||||||
// Mandatory
|
// Mandatory
|
||||||
'reporter': 'json:pretty',
|
|
||||||
'analyze-css': true,
|
'analyze-css': true,
|
||||||
'skip-modules': [
|
'ignoreSslErrors': true, // until Phantomas 2.1
|
||||||
'blockDomains', // not needed
|
'ignore-ssl-errors': true // for Phantomas >= 2.2
|
||||||
'caching', // overriden
|
|
||||||
'domMutations', // not compatible with webkit
|
|
||||||
'domQueries', // overriden
|
|
||||||
'eventListeners', // overridden
|
|
||||||
'filmStrip', // not needed
|
|
||||||
'har', // not needed for the moment
|
|
||||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
|
|
||||||
'jQuery', // overridden
|
|
||||||
'jserrors', // overridden
|
|
||||||
'pageSource', // not needed
|
|
||||||
'waitForSelector', // not needed
|
|
||||||
'windowPerformance' // overriden
|
|
||||||
].join(','),
|
|
||||||
'include-dirs': [
|
|
||||||
path.join(__dirname, 'custom_modules/core'),
|
|
||||||
path.join(__dirname, 'custom_modules/modules')
|
|
||||||
].join(',')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Output the command line for debugging purpose
|
|
||||||
debug('If you want to reproduce the phantomas task only, copy the following command line:');
|
|
||||||
var optionsString = '';
|
|
||||||
for (var opt in options) {
|
|
||||||
var value = options[opt];
|
|
||||||
|
|
||||||
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
|
|
||||||
value = '"' + value + '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsString += ' ' + '--' + opt + '=' + value;
|
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
|
|
||||||
|
|
||||||
// Kill the application if nothing happens
|
var debugCmd = 'DEBUG=* node node_modules/phantomas/bin/phantomas.js --url ' + task.url;
|
||||||
var phantomasPid;
|
Object.keys(options).forEach(function(key) {
|
||||||
var killer = setTimeout(function() {
|
if (key !== 'wait-for-network-idle' && options[key] !== null) {
|
||||||
debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
|
debugCmd += ' --' + key + ' ' + options[key];
|
||||||
// If in server mode, forever will restart the server
|
|
||||||
|
|
||||||
// Kill the Phantomas process first
|
|
||||||
if (phantomasPid) {
|
|
||||||
ps.kill(phantomasPid, function(err) {
|
|
||||||
if (err) {
|
|
||||||
debug('Could not kill Phantomas process %s', phantomasPid);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
debug('Phantomas process %s was correctly killed', phantomasPid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then suicide.
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, 5*options.timeout*1000);
|
debug('If you want to run Phantomas alone for debugging purpose, this is the command: %s', debugCmd);
|
||||||
|
|
||||||
// It's time to launch the test!!!
|
// It's time to launch the test!!!
|
||||||
var triesNumber = 2;
|
|
||||||
|
const promise = phantomas(task.url, options);
|
||||||
|
|
||||||
async.retry(triesNumber, function(cb) {
|
// handle the promise
|
||||||
var process = phantomas(task.url, options, function(err, json, results) {
|
promise.
|
||||||
debug('Returning from Phantomas');
|
then(results => {
|
||||||
|
var json = {
|
||||||
// Adding some YellowLabTools errors here
|
generator: results.getGenerator(),
|
||||||
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
|
url: results.getUrl(),
|
||||||
err = 1001;
|
metrics: results.getMetrics(),
|
||||||
}
|
offenders: results.getAllOffenders()
|
||||||
|
};
|
||||||
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) {
|
|
||||||
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
|
|
||||||
err = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
debug('Attempt failed. Error code ' + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(err, json);
|
|
||||||
});
|
|
||||||
|
|
||||||
phantomasPid = process.pid;
|
|
||||||
|
|
||||||
}, function(err, json) {
|
|
||||||
|
|
||||||
clearTimeout(killer);
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
debug('All ' + triesNumber + ' attemps failed for the test');
|
|
||||||
deferred.reject(err);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
deferred.resolve(json);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||