Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cee48c1f45 | ||
|
|
6d1ea96e79 | ||
|
|
5053f3f0f8 | ||
|
|
7b16c4ebe1 | ||
|
|
b8636029c9 | ||
|
|
357158db3e | ||
|
|
796ee5516f | ||
|
|
c82c1c192f | ||
|
|
c20cacac99 | ||
|
|
decfc5823d | ||
|
|
6da6377bb5 | ||
|
|
eedb46e000 | ||
|
|
fe8466b0e3 | ||
|
|
f10fbc3fa3 | ||
|
|
e186620a60 | ||
|
|
e1316ea48b | ||
|
|
a9adb3bb06 | ||
|
|
5ccab1770b | ||
|
|
3d377b7b1f | ||
|
|
8bdb578b90 | ||
|
|
50a9ea628d | ||
|
|
637ce5c1bc | ||
|
|
7432f4de08 | ||
|
|
594267b936 | ||
|
|
e210eca290 | ||
|
|
189ce3db64 | ||
|
|
07028ad3cd | ||
|
|
0d71781071 | ||
|
|
8e72efdd96 | ||
|
|
e0a656e2ba | ||
|
|
2afab43281 | ||
|
|
d1a09260d5 | ||
|
|
926db1c2b0 | ||
|
|
12dd676e9d | ||
|
|
dac72eec20 | ||
|
|
5e4d3c40f5 | ||
|
|
990237f975 | ||
|
|
4e29d5cbea | ||
|
|
9d58dc20cb | ||
|
|
b6bf77956b | ||
|
|
ae9e520df1 | ||
|
|
ae65c1af3d | ||
|
|
6f892c7dbf | ||
|
|
0c4fad6f54 | ||
|
|
1a9b749017 | ||
|
|
aee81cb824 | ||
|
|
6d6608754d | ||
|
|
326bfc035f | ||
|
|
3ff23e1c47 | ||
|
|
ada82fa7e1 | ||
|
|
51e4265109 | ||
|
|
eaebb3adfa | ||
|
|
76b4ca6ea5 | ||
|
|
b8560c6e15 | ||
|
|
8eec2549a4 | ||
|
|
aaa1ad7eaa | ||
|
|
74c57fc11c | ||
|
|
b7e25c4e1c | ||
|
|
3f97333423 | ||
|
|
71152f64a5 | ||
|
|
531d125dcc | ||
|
|
299e79e890 | ||
|
|
14f60d8dd3 | ||
|
|
a00b1a96c7 | ||
|
|
2847e02c5d | ||
|
|
57b30a1838 | ||
|
|
f2a637ada7 | ||
|
|
b3804586dc | ||
|
|
113c1f0565 | ||
|
|
3948eeaad4 | ||
|
|
f843184970 | ||
|
|
2aa9edeb8f | ||
|
|
5e37cf8224 | ||
|
|
8bd3a1edde | ||
|
|
c68962e2f2 | ||
|
|
6a9a247a87 | ||
|
|
e238370bc8 | ||
|
|
178f2fd135 | ||
|
|
55cd1160db | ||
|
|
06bdbd929c | ||
|
|
100eb01f27 | ||
|
|
00ade4ad29 | ||
|
|
f3a5779102 | ||
|
|
6f0b97e760 | ||
|
|
28f9ed64ff | ||
|
|
b6924f70d4 | ||
|
|
79ecae34c8 | ||
|
|
f4843baa0e | ||
|
|
cd97a275fe | ||
|
|
f2e77a5c3a | ||
|
|
23f4f9bd0b | ||
|
|
e8eb6964db | ||
|
|
9643c5bfcd | ||
|
|
56ebe6bb8b | ||
|
|
5f7c2f07b8 | ||
|
|
bbe965e7a3 | ||
|
|
ebd03c7b1a | ||
|
|
0d02a57600 | ||
|
|
394d3a5b25 | ||
|
|
4f9589fb6b | ||
|
|
048e3396de | ||
|
|
e3d378a4ba | ||
|
|
91f8d9c2b0 | ||
|
|
2285c896d2 | ||
|
|
9ff24918e2 | ||
|
|
e78e794240 | ||
|
|
0eb359542b | ||
|
|
c54cf5a2ba | ||
|
|
6f7f05b456 | ||
|
|
da6a8834dd | ||
|
|
7ed16d63f3 | ||
|
|
d8730cc67b | ||
|
|
b67efe0cac | ||
|
|
835cb2f90f | ||
|
|
f75e99f703 | ||
|
|
7e1bf0fc92 | ||
|
|
82693a8587 | ||
|
|
99eace0336 | ||
|
|
d0257fea8f | ||
|
|
453a5cda2e | ||
|
|
bdbd1a0f08 | ||
|
|
d32b9f78a4 | ||
|
|
46c2d8c49e | ||
|
|
88b5567131 | ||
|
|
0e81d16feb | ||
|
|
a900f0ff65 | ||
|
|
d4faa75b79 | ||
|
|
47b0de1b49 | ||
|
|
fe6a10592b | ||
|
|
8f02349ca5 | ||
|
|
1fb32de428 | ||
|
|
31b66ee6fa | ||
|
|
d22f9b81b2 | ||
|
|
3051d61365 | ||
|
|
53fd2e48e5 | ||
|
|
ef516c123d | ||
|
|
add8962390 | ||
|
|
1a444a481e | ||
|
|
96ca4a7933 | ||
|
|
f07bc615b0 | ||
|
|
2d0c88390a | ||
|
|
52574623fe | ||
|
|
b1d95ddf5f | ||
|
|
092bb285ed | ||
|
|
52ce26796a | ||
|
|
6bd55e4807 | ||
|
|
8325a9297e | ||
|
|
9cb3c78def | ||
|
|
143436ecd3 | ||
|
|
83cf87c8cf | ||
|
|
2f1003e8bc | ||
|
|
e0649eb9bd | ||
|
|
d5c607d370 | ||
|
|
2c2c8bf4cb | ||
|
|
08459ca289 | ||
|
|
162f1fa1d1 | ||
|
|
d9113edbbe | ||
|
|
eb1c5c07e9 | ||
|
|
0574429a93 | ||
|
|
8ff565404b | ||
|
|
b08d4b4cae | ||
|
|
f331662263 | ||
|
|
10693283c5 | ||
|
|
f2a933c51b | ||
|
|
172b8cfb9b | ||
|
|
47738ffa26 | ||
|
|
c8a29b4651 | ||
|
|
c305cc2f7d | ||
|
|
01676e1f99 | ||
|
|
de1e15fb25 | ||
|
|
0aa77c33ad | ||
|
|
22eeb1a47a | ||
|
|
15b3c6617b | ||
|
|
e342a88f7d | ||
|
|
7779efedf1 | ||
|
|
137b4d25b9 | ||
|
|
9be40adabf | ||
|
|
b6d4b7b40d | ||
|
|
3256789082 | ||
|
|
87adc575ac | ||
|
|
30ae0f93f3 | ||
|
|
c339654112 | ||
|
|
25c9770fba | ||
|
|
0bf7326604 | ||
|
|
7fc5a9ba8f | ||
|
|
230f805cae | ||
|
|
bc9c828584 | ||
|
|
6c17a709bf | ||
|
|
dd912ecc8f | ||
|
|
c9b12c4892 | ||
|
|
414aea2637 | ||
|
|
b7bc4dbadc | ||
|
|
8a8b871908 | ||
|
|
1225b27510 | ||
|
|
f86bd86043 | ||
|
|
8133164f44 | ||
|
|
2f447da34b | ||
|
|
41cdc8783e | ||
|
|
ccbb324270 | ||
|
|
8d4a0c34e7 | ||
|
|
2620f75955 | ||
|
|
4ca525f701 | ||
|
|
53a9aa377a | ||
|
|
d166003dfe | ||
|
|
31d80791a9 | ||
|
|
02245aa5f1 | ||
|
|
583b6b9a55 | ||
|
|
d4815e855e | ||
|
|
113665139a | ||
|
|
37e35b0ed6 | ||
|
|
eef8f00d8b | ||
|
|
bdb15a7780 | ||
|
|
2b59912549 | ||
|
|
8623085c2a | ||
|
|
612627d66f | ||
|
|
04700beab3 | ||
|
|
0d3d6c88e4 | ||
|
|
db57f67e98 | ||
|
|
baa145e15c | ||
|
|
c1bae20f01 | ||
|
|
41bc676b12 | ||
|
|
cea74dd63d | ||
|
|
9ccca3b33b | ||
|
|
1606cbab18 | ||
|
|
c428c8ae2d | ||
|
|
b52e84d1e7 | ||
|
|
3fbcbbe8e0 | ||
|
|
74eabd700c | ||
|
|
d7ddae38e5 | ||
|
|
fe7de2db99 | ||
|
|
9b9de42139 | ||
|
|
7b64288765 | ||
|
|
c6c967f89c | ||
|
|
040dba961a | ||
|
|
0146794cda | ||
|
|
b00eba77f9 | ||
|
|
3f27ae249a | ||
|
|
4384cffabe | ||
|
|
fde3234fc6 | ||
|
|
d998c7cd46 | ||
|
|
a8b62fc184 | ||
|
|
65de8e08d3 | ||
|
|
73cc1cd2e5 | ||
|
|
26926071b0 | ||
|
|
5c433129ec | ||
|
|
68bc1a6608 | ||
|
|
e67a0e0bc0 | ||
|
|
f553d45396 | ||
|
|
090a53b95a | ||
|
|
22ab7515fb | ||
|
|
a97dcc5efb | ||
|
|
4b098aef69 | ||
|
|
cb46274288 | ||
|
|
1e60a43075 | ||
|
|
1aa9cd032f | ||
|
|
5e3748315c | ||
|
|
7f67c032d1 | ||
|
|
33621af3ad | ||
|
|
2fb4a7bd6a | ||
|
|
1ff983a03b | ||
|
|
701530ef04 | ||
|
|
719e624ddf | ||
|
|
03a25df367 | ||
|
|
c93ce684a8 | ||
|
|
090708f38b | ||
|
|
49f93e660e | ||
|
|
867680f4e9 | ||
|
|
36ac529cc8 | ||
|
|
0ae388f0f6 | ||
|
|
b53ace765c | ||
|
|
f256299a0c | ||
|
|
4fb05e406a | ||
|
|
035639c68a | ||
|
|
09f53cac92 | ||
|
|
05a27d9003 | ||
|
|
5d4d1807ba | ||
|
|
836e465414 | ||
|
|
94457b24cd | ||
|
|
728aaec24d | ||
|
|
4ccd2d7087 | ||
|
|
9bc6d2928b | ||
|
|
d7d4fb36ad | ||
|
|
4e6a88abec | ||
|
|
a7051ace5d | ||
|
|
cee47f2846 | ||
|
|
f9e60acdf6 | ||
|
|
5f6ba048e6 | ||
|
|
aba91de480 | ||
|
|
118ec757a3 | ||
|
|
eaf712937b | ||
|
|
6c47c406cb | ||
|
|
eaeff3bf6a | ||
|
|
5b5fb3e32d | ||
|
|
07a8dd4912 | ||
|
|
a2f5127222 | ||
|
|
eab48ea202 | ||
|
|
00e2717653 | ||
|
|
d83f150810 | ||
|
|
2a7e6c2dd2 | ||
|
|
f8f792645b | ||
|
|
8c79f39389 | ||
|
|
f7da653e87 | ||
|
|
1ed753eff1 | ||
|
|
385de967a4 | ||
|
|
ac84019777 | ||
|
|
b0c6fe9474 | ||
|
|
94e2b0233a | ||
|
|
c202e3426b | ||
|
|
7e86e83173 | ||
|
|
604857648b | ||
|
|
76fb50473f | ||
|
|
ffee3b225f | ||
|
|
5f1dd42559 | ||
|
|
57a7c78ce2 | ||
|
|
33c6643388 | ||
|
|
74deba61e6 | ||
|
|
bb2f0e6c55 | ||
|
|
b2f6f68d1a | ||
|
|
fdde065a94 | ||
|
|
9231b0914b | ||
|
|
78f49ae7c9 | ||
|
|
95ff81d81f | ||
|
|
92b73f62ad | ||
|
|
1057836788 | ||
|
|
93f8702962 | ||
|
|
3d71454666 | ||
|
|
c0cc2744c6 | ||
|
|
65b1c34d06 | ||
|
|
67615719ae | ||
|
|
491c8ebe77 | ||
|
|
cae23d61e2 | ||
|
|
9cad9ad2f9 | ||
|
|
ba977d6b49 | ||
|
|
88bb2f91a5 | ||
|
|
dda11627f8 | ||
|
|
2df713ca0c | ||
|
|
486ede9396 | ||
|
|
72afcc3e5a | ||
|
|
ef2eaceb88 | ||
|
|
c2db1c70e3 | ||
|
|
13413899eb | ||
|
|
98f074e8d5 | ||
|
|
36a60bae4d | ||
|
|
7c5a5daf2f | ||
|
|
7b74df7f04 | ||
|
|
a6813a7ec1 | ||
|
|
484d7ee3b2 | ||
|
|
7a1bc282e9 | ||
|
|
f45480c0b9 | ||
|
|
23d980910d | ||
|
|
9b939aa8b4 | ||
|
|
838a9c3e7d | ||
|
|
f91de30717 | ||
|
|
0c3299573f | ||
|
|
13e2d2d597 | ||
|
|
a6bbc7119d | ||
|
|
20bf58b84f | ||
|
|
fe176c92c0 | ||
|
|
6009ed53b7 | ||
|
|
79c387ba45 | ||
|
|
dd767d6664 | ||
|
|
61202f1635 | ||
|
|
457d6cc54b | ||
|
|
bf8123b7c5 | ||
|
|
c98ede9b81 | ||
|
|
34671fca59 | ||
|
|
7ad9381aad | ||
|
|
7387eefb10 | ||
|
|
ca3403a5b2 | ||
|
|
8adc895c8c | ||
|
|
6fe49d06ac | ||
|
|
c47e6d0219 | ||
|
|
99c4b240ec | ||
|
|
0947724245 | ||
|
|
15653ef961 | ||
|
|
893c92266a | ||
|
|
50571995a4 | ||
|
|
dc2f6e17cd | ||
|
|
9a861b085a | ||
|
|
714cb318a9 | ||
|
|
2827463b41 | ||
|
|
121a8b11b8 | ||
|
|
3e0e31a5fe | ||
|
|
a7c96f77fe | ||
|
|
d3444e9aa0 | ||
|
|
71d2e61551 | ||
|
|
3756d1185f | ||
|
|
5a22eb9542 | ||
|
|
cb535f1cee | ||
|
|
f304e5efb7 | ||
|
|
5732a8ffc8 | ||
|
|
8b7ccb837a | ||
|
|
f4bb3fc0bc | ||
|
|
62602a88bc | ||
|
|
f63fa9faf2 | ||
|
|
839bfd4756 | ||
|
|
9392d21783 | ||
|
|
ca5594dfbb | ||
|
|
1b938417e5 | ||
|
|
f95e94ac6d | ||
|
|
e9bb72a416 | ||
|
|
2d003febc1 | ||
|
|
42c27af604 | ||
|
|
ee490fc660 | ||
|
|
10500c2fba | ||
|
|
c4011c0817 | ||
|
|
bc49bebb58 | ||
|
|
dcdd93bcaa | ||
|
|
d5fdc3ce63 | ||
|
|
c1b5a8625a | ||
|
|
dd1fd34865 | ||
|
|
9b9e65abd4 | ||
|
|
e5f917a185 | ||
|
|
368bf4a6d9 | ||
|
|
a7df7a5436 | ||
|
|
d3f5236073 | ||
|
|
db6fdbc121 | ||
|
|
dc9aa4d6b4 | ||
|
|
5f2553047d | ||
|
|
d556f84929 | ||
|
|
4345ccf356 | ||
|
|
4032468e10 | ||
|
|
5330f0f8e2 | ||
|
|
39e6ecc920 | ||
|
|
928e8f14ea | ||
|
|
0e4ffcee62 | ||
|
|
955f353a31 | ||
|
|
44df06fac7 | ||
|
|
f099f6f4a9 | ||
|
|
e9259356bd | ||
|
|
2264445772 | ||
|
|
47f0e1f1bf | ||
|
|
79d0898fb5 | ||
|
|
85d1119ad6 | ||
|
|
52e73ca7cd | ||
|
|
b5ac42792e | ||
|
|
6a844e163b | ||
|
|
efa4151850 | ||
|
|
55dc387d3c | ||
|
|
ae47333eef | ||
|
|
5bbe4cde77 | ||
|
|
0ff35d13e8 | ||
|
|
3059ab835b | ||
|
|
798f295912 | ||
|
|
71d115d161 | ||
|
|
d5368e5f90 | ||
|
|
d1f0b2e459 | ||
|
|
ed4ddc0a43 | ||
|
|
61c09a5b49 | ||
|
|
a6194f2535 | ||
|
|
901116fde6 | ||
|
|
d829525ab8 | ||
|
|
ee666a3a6f | ||
|
|
4a8a5829d7 | ||
|
|
fd2271f6d6 | ||
|
|
cac36727e8 | ||
|
|
e6bd0d0006 | ||
|
|
4ef83a844f | ||
|
|
23dc46d697 | ||
|
|
6fc502b0b2 | ||
|
|
a4f22a8542 | ||
|
|
bce7a1ad9a | ||
|
|
49a6e4633a | ||
|
|
41c02cb732 | ||
|
|
0f00111373 | ||
|
|
6521d24925 | ||
|
|
34468198ec | ||
|
|
ab30aae08e | ||
|
|
eb4d8cde7c | ||
|
|
95daa4b02e | ||
|
|
7e7331fb6f | ||
|
|
7485f00f2f | ||
|
|
c3704fa4ff | ||
|
|
db39e79cf7 | ||
|
|
7c8ffec254 | ||
|
|
7e3d5d17f8 | ||
|
|
bbfe228e4c | ||
|
|
a15e04c7d0 | ||
|
|
301b4de31d | ||
|
|
73f48b71a2 | ||
|
|
d7aa24b467 | ||
|
|
377dee7d83 | ||
|
|
079ec94197 | ||
|
|
aee0905ea5 | ||
|
|
5bc32ea89a | ||
|
|
a6162b8440 | ||
|
|
3f389fcf79 | ||
|
|
d78716c873 | ||
|
|
aabbf882df | ||
|
|
6b1b9252b3 | ||
|
|
e26fd5ed6f | ||
|
|
249cfa21e3 | ||
|
|
fb4d6afb07 | ||
|
|
909ffc1aac | ||
|
|
9ec5bd7183 | ||
|
|
dbde3de8e8 | ||
|
|
5c80f3197a | ||
|
|
17a644c5d8 | ||
|
|
9bf420d317 | ||
|
|
be2911707c | ||
|
|
f601044645 | ||
|
|
c55a83cbe2 | ||
|
|
ef9326abc6 | ||
|
|
a219c1b570 | ||
|
|
1920a3e802 | ||
|
|
67d0809e2f | ||
|
|
7c6676c981 | ||
|
|
519f2ba617 | ||
|
|
0b23fad909 | ||
|
|
6fe61a041e | ||
|
|
fb323c2086 | ||
|
|
dccb9bb49c | ||
|
|
e9505f31f7 | ||
|
|
c31e880f63 | ||
|
|
b064e73ef4 | ||
|
|
606c2c6f95 | ||
|
|
c0e02726be | ||
|
|
2ddf2e8341 | ||
|
|
5d2e647f10 | ||
|
|
a141122fbd | ||
|
|
b186677a53 | ||
|
|
c8ba9ef99a | ||
|
|
c994638168 | ||
|
|
b6df64337d | ||
|
|
1c0936d4c1 | ||
|
|
983671f5c3 | ||
|
|
d6f76de349 | ||
|
|
7aede20819 | ||
|
|
0e5c7e1215 | ||
|
|
3cd60e9a8d | ||
|
|
cf79e95c45 | ||
|
|
3c22cd216c | ||
|
|
cb20075599 | ||
|
|
c1257afc4c | ||
|
|
0862116a40 | ||
|
|
6eefc0a623 | ||
|
|
ed4f448370 | ||
|
|
3dc4f4641c | ||
|
|
95a5f5063e | ||
|
|
4cca167fc1 | ||
|
|
301ebd9b6f | ||
|
|
01fe0ca381 | ||
|
|
b6a9f5109c | ||
|
|
3320e993e7 | ||
|
|
239672116b | ||
|
|
4d8e71fff4 | ||
|
|
4babf6d30e | ||
|
|
9fe1808e1c | ||
|
|
ebb9dbc7f1 | ||
|
|
e2dc5e0705 | ||
|
|
f66f464199 | ||
|
|
35d42b9ec5 | ||
|
|
0776551aa7 | ||
|
|
482a890447 | ||
|
|
bc8ebd8db3 | ||
|
|
521a81ca1d | ||
|
|
c4a7b1f129 | ||
|
|
d712eb3a59 | ||
|
|
21cd0cb346 | ||
|
|
b1a0dfa7bd | ||
|
|
ffbedc1e02 | ||
|
|
0a647264d7 | ||
|
|
1385e28911 | ||
|
|
2ab843efdb | ||
|
|
90dac7dd19 | ||
|
|
792f7d8e21 | ||
|
|
5b0eb07d0d | ||
|
|
8abfe93df7 | ||
|
|
3ad6cdef51 | ||
|
|
0da2ad0c10 | ||
|
|
84faabca86 | ||
|
|
49015e3422 | ||
|
|
906f0876a0 | ||
|
|
e18cd87a07 | ||
|
|
35aed5fb55 | ||
|
|
6089e5fe20 | ||
|
|
2a316ebb1e | ||
|
|
1f83fe1b48 | ||
|
|
7d1bb4af5b | ||
|
|
f57fea0a54 | ||
|
|
13787708e9 | ||
|
|
7af3bc519a | ||
|
|
e3d96de203 | ||
|
|
ec08177607 | ||
|
|
362dc9bbcc | ||
|
|
a9fb0b3b08 | ||
|
|
c6b417ca81 | ||
|
|
4e64af0f10 | ||
|
|
2a2d99191d | ||
|
|
b0b6e71d2e | ||
|
|
041904f4c6 | ||
|
|
5b2bf8956a | ||
|
|
a1278fad49 | ||
|
|
d84b831922 | ||
|
|
fe37d4e91c | ||
|
|
f3a08df532 | ||
|
|
f9b0f6d613 | ||
|
|
b72e74e0d3 |
@@ -1,7 +1,8 @@
|
||||
node_modules
|
||||
bower_components
|
||||
.tmp
|
||||
.vagrant
|
||||
results/*
|
||||
coverage
|
||||
front/build
|
||||
|
||||
har.json
|
||||
@@ -1,8 +1,18 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "6.2"
|
||||
- "4.0"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- "npm install -g grunt-cli"
|
||||
- "npm install -g phantomjs"
|
||||
install: npm install
|
||||
before_script: grunt build
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
install:
|
||||
- "npm install"
|
||||
@@ -1,60 +1,135 @@
|
||||
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: {
|
||||
webfont: {
|
||||
icons: {
|
||||
src: ['app/public/fonts/svg-icons/*.svg'],
|
||||
destCss: 'app/public/styles/less/icons.less',
|
||||
destFonts: 'app/public/fonts/icons.woff',
|
||||
|
||||
// Optional: Custom routing of font filepaths for CSS
|
||||
cssRouter: function (fontpath) {
|
||||
var pathArray = fontpath.split('/');
|
||||
var fileName = pathArray[pathArray.length - 1];
|
||||
return '/public/fonts/' + fileName;
|
||||
src: 'front/src/fonts/svg-icons/*.svg',
|
||||
dest: 'tmp',
|
||||
destCss: 'front/src/less',
|
||||
options: {
|
||||
engine: 'node',
|
||||
types: 'woff',
|
||||
stylesheet: 'less',
|
||||
embed: true,
|
||||
htmlDemo: false,
|
||||
syntax: 'bootstrap'
|
||||
}
|
||||
}
|
||||
},
|
||||
less: {
|
||||
all: {
|
||||
files: {
|
||||
'app/public/styles/main.css': [ 'app/public/styles/less/main.less' ],
|
||||
'app/public/styles/index.css': [ 'app/public/styles/less/index.less' ],
|
||||
'app/public/styles/launchTest.css': [ 'app/public/styles/less/launchTest.less' ],
|
||||
'app/public/styles/results.css': [ 'app/public/styles/less/results.less' ]
|
||||
}
|
||||
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/*',
|
||||
'app/lib/*.js',
|
||||
'bin/*.js',
|
||||
'lib/**/*.js',
|
||||
'app/nodeControllers/*.js',
|
||||
'app/public/scripts/*.js',
|
||||
'phantomas_custom/**/*.js'
|
||||
'phantomas_custom/**/*.js',
|
||||
'test/api/*.js',
|
||||
'test/core/*.js',
|
||||
'test/fixtures/*.js',
|
||||
'front/src/js/**/*.js'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
icons: {
|
||||
src: ['tmp']
|
||||
tmp: {
|
||||
src: ['.tmp']
|
||||
},
|
||||
dev: {
|
||||
src: ['front/src/css']
|
||||
},
|
||||
coverage: {
|
||||
src: ['coverage/']
|
||||
src: ['.tmp', 'coverage/']
|
||||
},
|
||||
build: {
|
||||
src: ['front/build']
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
beforeCoverage: {
|
||||
files: [
|
||||
{src: ['bin/server.js'], dest: '.tmp/'}
|
||||
]
|
||||
},
|
||||
coverage: {
|
||||
src: ['test/**'],
|
||||
dest: 'coverage/'
|
||||
files: [
|
||||
{cwd: 'test', src: '**/*', dest: 'coverage/test', expand: true},
|
||||
{cwd: 'lib/metadata', src: '**/*', dest: 'coverage/lib/metadata', expand: true},
|
||||
{cwd: 'node_modules/phantomas', src: '**/*', dest: 'coverage/node_modules/phantomas', expand: true},
|
||||
{cwd: 'lib/tools/phantomas/custom_modules', src: '**/*', dest: 'coverage/lib/tools/phantomas/custom_modules', expand: true}
|
||||
]
|
||||
},
|
||||
build: {
|
||||
files: [
|
||||
{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: {
|
||||
coverage: {
|
||||
coverageApp: {
|
||||
src: ['app/'],
|
||||
dest: 'coverage/app/'
|
||||
},
|
||||
coverageLib: {
|
||||
src: ['lib/'],
|
||||
dest: 'coverage/lib/'
|
||||
},
|
||||
coverageBin: {
|
||||
src: ['.tmp/bin/'],
|
||||
dest: 'coverage/bin/'
|
||||
}
|
||||
},
|
||||
mochaTest: {
|
||||
@@ -62,44 +137,220 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['coverage/test/server/*.js']
|
||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
||||
},
|
||||
'test-current-work': {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['test/core/imageOptimizerTest.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
reporter: 'html-cov',
|
||||
// use the quiet flag to suppress the mocha console output
|
||||
quiet: true,
|
||||
// specify a destination file to capture the mocha
|
||||
// output (the quiet option does not suppress this)
|
||||
captureFile: 'coverage/coverage.html'
|
||||
},
|
||||
src: ['coverage/test/server/*.js']
|
||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
||||
}
|
||||
},
|
||||
env: {
|
||||
dev: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
built: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
},
|
||||
express: {
|
||||
dev: {
|
||||
options: {
|
||||
port: 8383,
|
||||
server: './bin/server.js',
|
||||
serverreload: true,
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
built: {
|
||||
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']
|
||||
}
|
||||
},
|
||||
htmlmin: {
|
||||
options: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: 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/*/*.*'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
|
||||
|
||||
// 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',
|
||||
'webfont:icons',
|
||||
'less',
|
||||
'clean:icons'
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
grunt.registerTask('build', [
|
||||
'jshint',
|
||||
'less'
|
||||
'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('built', [
|
||||
'env:built',
|
||||
'express:built'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
'build',
|
||||
'express:testSuite',
|
||||
'clean:coverage',
|
||||
'copy-test-server-settings',
|
||||
'lineremover:beforeCoverage',
|
||||
'copy:beforeCoverage',
|
||||
'blanket',
|
||||
'copy:coverage',
|
||||
'mochaTest'
|
||||
'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,37 +1,89 @@
|
||||
# Yellow Lab Tools
|
||||
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
|
||||
|
||||
Online tool designed to help Front-End developers optimize their website
|
||||
|
||||
[](https://gemnasium.com/gmetais/YellowLabTools) [](https://travis-ci.org/gmetais/YellowLabTools)
|
||||
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
|
||||
|
||||
|
||||
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
|
||||
#
|
||||
|
||||
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>online tool</b> (recommended):
|
||||
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-animated.gif"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/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>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
|
||||
<br>For developers or Continuous Integration
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/grunt-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/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/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
|
||||
<br>
|
||||
Allows you to launch runs from any language
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/api-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The tool is based on the fabulous [Phantomas](https://github.com/macbre/phantomas) by Maciej Brencz, that loads a page and collects many web performance metrics.
|
||||
I rewrote some modules to deeper analyze Javascript interactions with the DOM (especially with jQuery).
|
||||
And then it is wrapped inside a small NodeJS server to get an interface (inspired by my favorite tool: [WebPageTest](http://www.webpagetest.org/)).
|
||||
The tool loads the given URL via [PhantomJS](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 then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
|
||||
|
||||
**There are so many things left to do, your help would be greatly appreciated! Please report bugs, ask for evolutions and come code with me.**
|
||||
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
|
||||
|
||||

|
||||
|
||||
|
||||
## Help needed
|
||||
## Test your localhost
|
||||
|
||||
I'm currently looking for help in correcting the english wordings in the tool, as it is not my native language.
|
||||
You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
|
||||
|
||||
|
||||
## Install your own 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-own-instance).
|
||||
|
||||
|
||||
## License
|
||||
Please read the [license](LICENSE).
|
||||
|
||||
Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
||||
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).
|
||||
|
||||
|
||||
## 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 YellowLabTools!
|
||||
|
||||
I can also help your company about Web Performances, visit [my website](https://www.gaelmetais.com).
|
||||
|
||||
|
||||
## Contributors
|
||||
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
|
||||
- [camlafit](https://github.com/camlafit)
|
||||
- Vincent L. [magikcypress](https://github.com/magikcypress)
|
||||
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
Vagrant.configure("2") do |config|
|
||||
|
||||
config.vm.box = "precise64"
|
||||
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
|
||||
config.vm.box = "ubuntu/precise64"
|
||||
|
||||
config.vm.network :private_network, ip: "10.10.10.10"
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
# http://foo-o-rama.com/vagrant--stdin-is-not-a-tty--fix.html
|
||||
config.vm.provision "fix-no-tty", type: "shell" do |s|
|
||||
s.privileged = false
|
||||
s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile"
|
||||
end
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
vb.customize ["modifyvm", :id, "--memory", 512]
|
||||
vb.customize ["modifyvm", :id, "--memory", 1024]
|
||||
vb.customize ["modifyvm", :id, "--cpus", 2]
|
||||
end
|
||||
|
||||
config.vm.provision :shell, :path => "server_config/server_install.sh"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Yellow Lab Tools main file
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var phantomas = require('phantomas');
|
||||
|
||||
var PhantomasWrapper = function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This is the phantomas launcher. It merges user chosen options into the default options
|
||||
* Available options :
|
||||
*
|
||||
* - timeout : in seconds (default 60)
|
||||
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree (reported durations of main tasks will be slower than usual)
|
||||
*
|
||||
*/
|
||||
this.execute = function(task, callback) {
|
||||
|
||||
var options = {
|
||||
// Cusomizable options
|
||||
timeout: task.options.timeout || 60,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
|
||||
// Mandatory
|
||||
reporter: 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
'domComplexity', // overriden
|
||||
'domMutations', // not compatible with webkit
|
||||
'domQueries', // overriden
|
||||
'eventListeners', // overridden
|
||||
'filmStrip', // not needed
|
||||
'har', // not needed for the moment
|
||||
'pageSource', // not needed
|
||||
'screenshot', // not needed for the moment
|
||||
'waitForSelector', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
'include-dirs': [
|
||||
'phantomas_custom/core',
|
||||
'phantomas_custom/modules'
|
||||
].join(',')
|
||||
};
|
||||
|
||||
// Output the command line for debugging purpose
|
||||
console.log('If you want to reproduce the phantomas task only, copy the following command line:');
|
||||
var optionsString = '';
|
||||
for (var opt in options) {
|
||||
optionsString += ' ' + '--' + opt + '=' + options[opt];
|
||||
}
|
||||
console.log('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
|
||||
|
||||
// Kill the application if nothing happens for 10 minutes
|
||||
var killer = setTimeout(function() {
|
||||
console.log('Killing the server because the test ' + task.testId + ' on ' + task.url + ' was launched 10 minutes ago');
|
||||
// Forever will restart the server
|
||||
process.exit(1);
|
||||
}, 600000);
|
||||
|
||||
// It's time to launch the test!!!
|
||||
var triesNumber = 3;
|
||||
|
||||
async.retry(triesNumber, function(cb) {
|
||||
phantomas(task.url, options, function(err, json, results) {
|
||||
console.log('Returning from Phantomas');
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
|
||||
err = 1001;
|
||||
}
|
||||
|
||||
if (!err && (!json || !json.metrics)) {
|
||||
err = 1002;
|
||||
}
|
||||
|
||||
// Don't cancel test if it is a timeout and we've got some results
|
||||
if (err === 252 && json) {
|
||||
console.log('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
|
||||
err = null;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
console.log('Attempt failed for test id ' + task.testId + '. Error code ' + err);
|
||||
}
|
||||
|
||||
cb(err, {json: json, results: results});
|
||||
});
|
||||
}, function(err, data) {
|
||||
|
||||
clearTimeout(killer);
|
||||
|
||||
if (err) {
|
||||
console.log('All ' + triesNumber + ' attemps failed for test id ' + task.testId);
|
||||
}
|
||||
callback(err, data.json, data.results);
|
||||
});
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new PhantomasWrapper();
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Alternative to the standard String.prototype.replace function
|
||||
* Avoids problems with $$, $1, $2, ...
|
||||
*/
|
||||
|
||||
module.exports = function(str, searched, replacement) {
|
||||
return str.split(searched).join(replacement);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Creation of a queue and it's worker function
|
||||
*/
|
||||
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var phantomasWrapper = require('./phantomasWrapper');
|
||||
|
||||
|
||||
var testQueue = function() {
|
||||
'use strict';
|
||||
|
||||
var currentTask = null;
|
||||
var self = this;
|
||||
|
||||
var queue = async.queue(function(task, callback) {
|
||||
currentTask = task;
|
||||
|
||||
console.log('Starting test ' + task.testId);
|
||||
|
||||
phantomasWrapper.execute(task, function(err, json, results) {
|
||||
console.log('Test ' + task.testId + ' complete');
|
||||
currentTask = null;
|
||||
callback(err, json, results);
|
||||
self.emit('queueMoving');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Use this method to add a test to the queue
|
||||
this.push = queue.push;
|
||||
|
||||
|
||||
// Gives the position of a task in the queue
|
||||
// Returns 0 if it is the current running task
|
||||
// Returns -1 if not found
|
||||
this.indexOf = function(testId) {
|
||||
if (currentTask && currentTask.testId === testId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var position = -1;
|
||||
if (queue.length() > 0) {
|
||||
queue.tasks.forEach(function(task, index) {
|
||||
if (task.data.testId === testId) {
|
||||
position = index + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
this.testComplete = function(testId) {
|
||||
self.emit('testComplete', testId);
|
||||
};
|
||||
|
||||
this.testFailed = function(testId) {
|
||||
self.emit('testFailed', testId);
|
||||
};
|
||||
};
|
||||
|
||||
// extend the EventEmitter class
|
||||
util.inherits(testQueue, EventEmitter);
|
||||
|
||||
module.exports = new testQueue();
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Yellow Lab Tools home page controller
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require ('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var indexController = function(req, res, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
async.parallel({
|
||||
|
||||
htmlTemplate: function(callback) {
|
||||
fs.readFile('./app/node_views/index.html', {encoding: 'utf8'}, callback);
|
||||
}
|
||||
|
||||
}, function(err, results) {
|
||||
var html = results.htmlTemplate;
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = indexController;
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Controller for the test launching page (the waiting page, after the user submited a test on the index page)
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require ('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var launchTestController = function(req, res, testQueue, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
// Generate test id
|
||||
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
|
||||
|
||||
var resultsPath = 'results/' + testId;
|
||||
var phantomasResultsPath = resultsPath + '/results.json';
|
||||
|
||||
var url = req.body.url;
|
||||
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
var options = {};
|
||||
if (req.body.timeout) {
|
||||
options.timeout = req.body.timeout;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
|
||||
function htmlTemplate(callback) {
|
||||
fs.readFile('./app/node_views/launchTest.html', {encoding: 'utf8'}, callback);
|
||||
},
|
||||
|
||||
function sendResponse(html, callback) {
|
||||
|
||||
html = strReplace(html, '%%TEST_URL%%', url);
|
||||
html = strReplace(html, '%%TEST_ID%%', testId);
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
function createFolder(callback) {
|
||||
// Create results folder
|
||||
fs.mkdir(resultsPath, callback);
|
||||
},
|
||||
|
||||
function executePhantomas(callback) {
|
||||
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
|
||||
|
||||
var task = {
|
||||
testId: testId,
|
||||
url: url,
|
||||
options: options
|
||||
};
|
||||
|
||||
testQueue.push(task, callback);
|
||||
},
|
||||
|
||||
function writeResults(json, resultsObject, callback) {
|
||||
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
|
||||
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
|
||||
}
|
||||
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.log('An error occured in the phantomas test: ', err);
|
||||
|
||||
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
|
||||
if (err) {
|
||||
console.log('Could not even write an error message on file ' + phantomasResultsPath);
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
testQueue.testFailed(testId);
|
||||
} else {
|
||||
testQueue.testComplete(testId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = launchTestController;
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* The page that dispays the results
|
||||
*/
|
||||
|
||||
var async = require('async');
|
||||
var fs = require('fs');
|
||||
var strReplace = require('../lib/strReplace');
|
||||
|
||||
var resultsController = function(req, res, googleAnalyticsId) {
|
||||
'use strict';
|
||||
|
||||
var testId = req.params.testId;
|
||||
var resultsPath = 'results/' + testId;
|
||||
var phantomasResultsPath = resultsPath + '/results.json';
|
||||
|
||||
console.log('Opening test ' + testId + ' results as HTML');
|
||||
|
||||
async.parallel({
|
||||
|
||||
htmlTemplate: function(callback) {
|
||||
fs.readFile('./app/node_views/results.html', {encoding: 'utf8'}, callback);
|
||||
},
|
||||
|
||||
phantomasResults: function(callback) {
|
||||
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
|
||||
}
|
||||
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return res.status(404).send('Sorry, test not found...');
|
||||
}
|
||||
|
||||
// Escape "</script>" because it can interfer with the HTML parser
|
||||
var phantomasResults = results.phantomasResults;
|
||||
phantomasResults = phantomasResults.replace(/<\/script>/g, '\\u003c/script>');
|
||||
|
||||
var html = results.htmlTemplate;
|
||||
html = strReplace(html, '%%RESULTS%%', phantomasResults);
|
||||
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = resultsController;
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Socket.io handler
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var waitingQueueSocket = function(socket, testQueue) {
|
||||
|
||||
socket.on('waiting', function(testId) {
|
||||
console.log('User waiting for test id ' + testId);
|
||||
|
||||
sendTestStatus(testId);
|
||||
|
||||
testQueue.on('testComplete', function(id) {
|
||||
if (testId === id) {
|
||||
socket.emit('complete');
|
||||
console.log('Sending complete event to test id ' + testId);
|
||||
}
|
||||
});
|
||||
|
||||
testQueue.on('testFailed', function(id) {
|
||||
if (testId === id) {
|
||||
socket.emit('failed');
|
||||
console.log('Sending failed event to test id ' + testId);
|
||||
}
|
||||
});
|
||||
|
||||
testQueue.on('queueMoving', function() {
|
||||
var positionInQueue = testQueue.indexOf(testId);
|
||||
if (positionInQueue >= 0) {
|
||||
socket.emit('position', positionInQueue);
|
||||
console.log('Sending position to test id ' + testId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finds the status of a test and send it to the client
|
||||
function sendTestStatus(testId) {
|
||||
// Check task position in queue
|
||||
var positionInQueue = testQueue.indexOf(testId);
|
||||
|
||||
if (positionInQueue >= 0) {
|
||||
socket.emit('position', positionInQueue);
|
||||
} else {
|
||||
// Find in results files
|
||||
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
|
||||
if (exists) {
|
||||
// TODO : find a way to make sure the file is completely written
|
||||
setTimeout(function() {
|
||||
socket.emit('complete');
|
||||
}, 4000);
|
||||
} else {
|
||||
socket.emit('404');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = waitingQueueSocket;
|
||||
@@ -1,35 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
|
||||
<form method="post" action="/launchTest">
|
||||
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
|
||||
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
|
||||
</form>
|
||||
<p>(This is a BETA, your feedback is more than welcome)</p>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,97 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools - Awaiting</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<div>%%TEST_URL%%</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var testId = '%%TEST_ID%%';
|
||||
var statusElement = document.getElementById('status');
|
||||
var socket = io();
|
||||
|
||||
function askStatus() {
|
||||
socket.emit('waiting', testId);
|
||||
}
|
||||
|
||||
socket.on('position', function(position) {
|
||||
if (position === 0) {
|
||||
statusElement.innerHTML = 'Test is running...';
|
||||
} else {
|
||||
statusElement.innerHTML = 'Waiting behind ' + (position) + ' other tests';
|
||||
}
|
||||
sendPositionInQueueToGA(position);
|
||||
|
||||
// Rotate the spinner
|
||||
document.body.className = 'waiting';
|
||||
});
|
||||
|
||||
socket.on('complete', function() {
|
||||
statusElement.innerHTML = 'Test complete';
|
||||
window.location.replace('/results/' + testId);
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('failed', function() {
|
||||
statusElement.innerHTML = 'Test failed';
|
||||
window.location.replace('/results/' + testId);
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('404', function() {
|
||||
statusElement.innerHTML = 'Test not found';
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', function() {
|
||||
statusElement.innerHTML = "Server reboot. Please start a new test."
|
||||
|
||||
// Stop the spinner
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
askStatus();
|
||||
|
||||
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
}
|
||||
|
||||
// Need stats on average queue position
|
||||
var positionAlreadySent = false;
|
||||
function sendPositionInQueueToGA(position) {
|
||||
if (!positionAlreadySent && GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
ga('send', 'pageview', {
|
||||
queuePosition: position
|
||||
});
|
||||
positionAlreadySent = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,933 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools - Results page</title>
|
||||
<link rel="icon" type="image/png" href="/public/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/ngModal/dist/ng-modal.js"></script>
|
||||
<script src="/public/scripts/app.js"></script>
|
||||
<script src="/public/scripts/resultsCtrl.js"></script>
|
||||
<script src="/public/scripts/showOffenders.js"></script>
|
||||
</head>
|
||||
<body ng-app="YellowLabTools" ng-controller="ResultsCtrl">
|
||||
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
|
||||
|
||||
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
|
||||
|
||||
<div class="ng-cloak">
|
||||
<div>Tested url: <a class="testedUrl" href="{{phantomasResults.url}}" target="_blank">{{phantomasResults.url}}</a></div>
|
||||
|
||||
<div class="resultsMenu">
|
||||
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
|
||||
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'execution'}" ng-click="setView('execution')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
|
||||
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'summary'}" ng-click="setView('summary')"><div class="icon-summary"></div><span>Grades</span></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="phantomasResults.error">
|
||||
<h2>Error: {{phantomasResults.error}}</h2>
|
||||
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
|
||||
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
|
||||
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
|
||||
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
|
||||
<div ng-if="phantomasResults.error == 1001">Javascript execution tree error</div>
|
||||
<div ng-if="phantomasResults.error == 1002">JSON undefined error</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="view == 'summary' && !phantomasResults.error" class="summary">
|
||||
<h2>Grades</h2>
|
||||
|
||||
<div class="notations">
|
||||
<div>
|
||||
<div ng-class="notations.domComplexity">{{notations.domComplexity}}</div>
|
||||
<div class="notation">DOM complexity</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.DOMelementsCount" ng-class="{'warning': phantomasResults.metrics.DOMelementsCount > 5000}">
|
||||
<div class="label">DOM elements count</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMelementsCount}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementsCountTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementsCountTooltip" dialog-title="Avoid too many DOM elements">
|
||||
<p>A high number of DOM elements means a lot of work for the browser to render the page.</p>
|
||||
<p>It also slows down Javascript DOM queries, as there are more elements to search through.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMelementMaxDepth" ng-class="{'warning': phantomasResults.metrics.DOMelementMaxDepth > 30}">
|
||||
<div class="label">DOM max depth</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMelementMaxDepth}}
|
||||
<show-offenders modal-title="DOM max depth" metric-name="DOMelementMaxDepth" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="Reduce DOM depth">
|
||||
<p>A deep DOM makes the CSS matching with DOM elements difficult.</p>
|
||||
<p>It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.iframesCount" ng-class="{'warning': phantomasResults.metrics.iframesCount > 30}">
|
||||
<div class="label">Number of iframes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.iframesCount}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
|
||||
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="iFrames are slow">
|
||||
<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMidDuplicated" ng-class="{'warning': phantomasResults.metrics.DOMidDuplicated > 20}">
|
||||
<div class="label">IDs duplicated</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMidDuplicated}}
|
||||
<show-offenders modal-title="DOM IDs duplicated" metric-name="DOMidDuplicated" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMidDuplicatedTooltip = true"></div>
|
||||
<modal-dialog show="DOMidDuplicatedTooltip" dialog-title="Warning: duplicated DOM IDs">
|
||||
<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jsDomManipulations">{{notations.jsDomManipulations}}</div>
|
||||
<div class="notation">DOM manipulations</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.DOMinserts" ng-class="{'warning': phantomasResults.metrics.DOMinserts > 1000}">
|
||||
<div class="label">DOM inserts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMinserts}}
|
||||
<show-offenders modal-title="DOM inserts" metric-name="DOMinserts" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMinsertsTooltip = true"></div>
|
||||
<modal-dialog show="DOMinsertsTooltip" dialog-title="DOM insertions are slow">
|
||||
<p>Working with the DOM in Javascript triggers layout calculations and slows down the page.</p>
|
||||
<p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMqueries" ng-class="{'warning': phantomasResults.metrics.DOMqueries > 2000}">
|
||||
<div class="label">DOM queries</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMqueries}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="DOMqueriesTooltip = true"></div>
|
||||
<modal-dialog show="DOMqueriesTooltip" dialog-title="Avoid having too many DOM queries">
|
||||
<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p>
|
||||
<p>Try to reduce the number of queries by refactoring your Javascript code.</p>
|
||||
<p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.DOMqueriesAvoidable" ng-class="{'warning': phantomasResults.metrics.DOMqueriesAvoidable > 500}">
|
||||
<div class="label">Avoidable queries</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.DOMqueriesAvoidable}}
|
||||
<show-offenders modal-title="Duplicated DOM queries" metric-name="DOMqueriesDuplicated" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="duplicatedQueriesCountAllTooltip = true"></div>
|
||||
<modal-dialog show="duplicatedQueriesCountAllTooltip" dialog-title="Cache duplicated DOM queries">
|
||||
<p>This is the number of queries that could be avoided by removing all duplicated queries.</p>
|
||||
<p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.eventsBound" ng-class="{'warning': phantomasResults.metrics.eventsBound > 2000}">
|
||||
<div class="label">Events bound</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.eventsBound}}
|
||||
<show-offenders modal-title="Events bound" metric-name="eventsBound" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="eventsBoundTooltip = true"></div>
|
||||
<modal-dialog show="eventsBoundTooltip" dialog-title="Reduce the number of events binding">
|
||||
<p>Binding too many events has a cost.</p>
|
||||
<p>It can be avoided by using "event delegation". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jsBadPractices">{{notations.jsBadPractices}}</div>
|
||||
<div class="notation">Bad Javascript</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.jsErrors" ng-class="{'warning': phantomasResults.metrics.jsErrors > 5}">
|
||||
<div class="label">Javascript errors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsErrors}}
|
||||
<show-offenders modal-title="Javascript errors" metric-name="jsErrors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jsErrorsTooltip = true"></div>
|
||||
<modal-dialog show="jsErrorsTooltip" dialog-title="Javascript errors">
|
||||
<p>Just to let you know there are some errors on the page.</p>
|
||||
<p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.evalCalls">
|
||||
<div class="label">eval calls</div>
|
||||
<div class="result">{{phantomasResults.metrics.evalCalls}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="evalCallsTooltip = true"></div>
|
||||
<modal-dialog show="evalCallsTooltip" dialog-title="Eval is evil">
|
||||
<p>The 'eval' function is slow and a bad coding practice. Try to get rid of it.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.documentWriteCalls">
|
||||
<div class="label">document.write calls</div>
|
||||
<div class="result">{{phantomasResults.metrics.documentWriteCalls}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="documentWriteCallsTooltip = true"></div>
|
||||
<modal-dialog show="documentWriteCallsTooltip" dialog-title="document.write should have never existed">
|
||||
<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p>
|
||||
<p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href="https://github.com/krux/postscribe" target="_blank">PostScribe</a>.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.consoleMessages">
|
||||
<div class="label">Console messages</div>
|
||||
<div class="result">{{phantomasResults.metrics.consoleMessages}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="consoleMessagesTooltip = true"></div>
|
||||
<modal-dialog show="consoleMessagesTooltip" dialog-title="Avoid using console.log()">
|
||||
<p>Try to keep your console clean when in production. Debugging is good for development only.</p>
|
||||
<p>Writing in the console has a cost, especially when dumping large object variables.</p>
|
||||
<p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.globalVariables">
|
||||
<div class="label">Global variables</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.globalVariables}}
|
||||
<show-offenders modal-title="Global variables" metric-name="globalVariables" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="globalVariablesTooltip = true"></div>
|
||||
<modal-dialog show="globalVariablesTooltip" dialog-title="Minimize the use of global variables">
|
||||
<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p>
|
||||
<p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="inBodyDomManipulations > 10" ng-class="{'warning': inBodyDomManipulations > 200}">
|
||||
<div class="label">DOM manipulations in body</div>
|
||||
<div class="result">
|
||||
{{inBodyDomManipulations}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="inBodyDomManipulationsTooltip = true"></div>
|
||||
<modal-dialog show="inBodyDomManipulationsTooltip" dialog-title="Javascript executed inside the body">
|
||||
<p>This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.</p>
|
||||
<p>Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.</p>
|
||||
<p>The JS Timeline tab can help you identify what's happening.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.jQueryLoading">{{notations.jQueryLoading}}</div>
|
||||
<div class="notation">jQuery version</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions == 1">
|
||||
<div class="label">jQuery version</div>
|
||||
<div class="result">{{phantomasResults.metrics.jQueryVersion}}</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jQueryVersionTooltip = true"></div>
|
||||
<modal-dialog show="jQueryVersionTooltip" dialog-title="Use the latest version of jQuery">
|
||||
<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p>
|
||||
<p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions > 1" class="warning">
|
||||
<div class="label">{{phantomasResults.metrics.jQueryDifferentVersions}} versions loaded</div>
|
||||
<div class="result">
|
||||
<span ng-repeat="version in phantomasResults.offenders.jQueryDifferentVersions">{{version}}<span ng-show="!$last"> & </span></span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="jQueryDifferentVersionsTooltip = true"></div>
|
||||
<modal-dialog show="jQueryDifferentVersionsTooltip" dialog-title="Several versions of jQuery loaded">
|
||||
<p>jQuery is a heavy library. You should **never** load jQuery more than one on the same page.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.cssComplexity">{{notations.cssComplexity}}</div>
|
||||
<div class="notation">CSS complexity</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.cssRules" ng-class="{'warning': phantomasResults.metrics.cssRules > 4000 || (phantomasResults.metrics.cssRules > 1000 && phantomasResults.metrics.cssRules > phantomasResults.metrics.DOMelementsCount)}">
|
||||
<div class="label">Rules count</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRules}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssRulesTooltip" dialog-title="Clean up the CSS files">
|
||||
<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p>
|
||||
<p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom files per page.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssComplexSelectors" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectors > 2000}">
|
||||
<div class="label">Complex selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssComplexSelectors}}
|
||||
<show-offenders modal-title="Complex selectors" metric-name="cssComplexSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssComplexSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssComplexSelectorsTooltip" dialog-title="Avoid complex selectors">
|
||||
<p>Complex selectors are CSS selectors with 4 or more expressions, like "#header ul li .foo".</p>
|
||||
<p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssComplexSelectorsByAttribute" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectorsByAttribute > 100}">
|
||||
<div class="label">Complex attributes selector</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssComplexSelectorsByAttribute}}
|
||||
<show-offenders modal-title="Complex attributes selector" metric-name="cssComplexSelectorsByAttribute" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssComplexSelectorsByAttributeTooltip = true"></div>
|
||||
<modal-dialog show="cssComplexSelectorsByAttributeTooltip" dialog-title="Avoid complex attribute expressions">
|
||||
<p>Complex attributes selectors are one of these:
|
||||
<ul>
|
||||
<li>.foo[type*=bar] (contains bar)</li>
|
||||
<li>.foo[type^=bar] (starts with bar)</li>
|
||||
<li>.foo[type|=bar] (starts with bar or bar-)</li>
|
||||
<li>.foo[type$=bar] (ends with bar)</li>
|
||||
<li>.foo[type~=bar baz] (bar or baz)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>Their matching process needs more CPU and it has a cost on performances.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.badCss">{{notations.badCss}}</div>
|
||||
<div class="notation">Bad CSS</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.cssParsingErrors" class="warning">
|
||||
<div class="label">CSS syntax error</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssParsingErrors}}
|
||||
<show-offenders modal-title="CSS syntax errors" metric-name="cssParsingErrors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssRulesTooltip" dialog-title="Parsing error while analyzing CSS">
|
||||
<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p>
|
||||
<p>Maybe a <a href="http://jigsaw.w3.org/css-validator" target="_blank">CSS validator</a> can help you.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssImports" class="warning">
|
||||
<div class="label">Uses of @import</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssImports}}
|
||||
<show-offenders modal-title="Uses of @import" metric-name="cssImports" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssImportsTooltip = true"></div>
|
||||
<modal-dialog show="cssImportsTooltip" dialog-title="Don't use @import">
|
||||
<p>It’s bad for performance to use @import because CSS files don't get downloaded in parallel.</p>
|
||||
<p>You should use <link rel='stylesheet' href='a.css'> instead.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssDuplicatedSelectors" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedSelectors > 80}">
|
||||
<div class="label">Duplicated selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssDuplicatedSelectors}}
|
||||
<show-offenders modal-title="Duplicated selectors" metric-name="cssDuplicatedSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssDuplicatedSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssDuplicatedSelectorsTooltip" dialog-title="Merge duplicated selectors">
|
||||
<p>This is when two or more selectors are strictly identical and should be merged.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssDuplicatedProperties" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedProperties > 100}">
|
||||
<div class="label">Duplicated properties</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssDuplicatedProperties}}
|
||||
<show-offenders modal-title="Duplicated properties" metric-name="cssDuplicatedProperties" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssDuplicatedPropertiesTooltip = true"></div>
|
||||
<modal-dialog show="cssDuplicatedPropertiesTooltip" dialog-title="Remove duplicated properties">
|
||||
<p>This is the number of property definitions duplicated within a selector.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssEmptyRules" ng-class="{'warning': phantomasResults.metrics.cssEmptyRules > 100}">
|
||||
<div class="label">Empty rules</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssEmptyRules}}
|
||||
<show-offenders modal-title="Empty rules" metric-name="cssEmptyRules" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssEmptyRulesTooltip = true"></div>
|
||||
<modal-dialog show="cssEmptyRulesTooltip" dialog-title="Eliminate empty rules">
|
||||
<p>Very easy to fix.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssExpressions">
|
||||
<div class="label">CSS expressions</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssExpressions}}
|
||||
<show-offenders modal-title="CSS expressions" metric-name="cssExpressions" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssExpressionsTooltip = true"></div>
|
||||
<modal-dialog show="cssExpressionsTooltip" dialog-title="Number of rules with CSS expressions">
|
||||
<p>Such as: expression( document.body.clientWidth > 600 ? "600px" : "auto" )</p>
|
||||
<p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssImportants">
|
||||
<div class="label">Uses of !important</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssImportants}}
|
||||
<show-offenders modal-title="Uses of !important" metric-name="cssImportants" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssImportantsTooltip = true"></div>
|
||||
<modal-dialog show="cssImportantsTooltip" dialog-title="Don't over-use !important">
|
||||
<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssOldIEFixes">
|
||||
<div class="label">Old IE fixes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssOldIEFixes}}
|
||||
<show-offenders modal-title="Old IE fixes" metric-name="cssOldIEFixes" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssOldIEFixesTooltip = true"></div>
|
||||
<modal-dialog show="cssOldIEFixesTooltip" dialog-title="Clean up old IE fixes">
|
||||
<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p>
|
||||
<p>IE6:
|
||||
<ul>
|
||||
<li>* html</li>
|
||||
<li>html > body (everything but IE6)</li>
|
||||
</ul>
|
||||
<p>
|
||||
<p>IE7:
|
||||
<ul>
|
||||
<li><b>*</b>height: 123px;</li>
|
||||
<li>height: 123px <b>!ie</b>;</li>
|
||||
</ul>
|
||||
<p>
|
||||
<p>IE9:
|
||||
<ul>
|
||||
<li>-ms-filter</li>
|
||||
<li>progid:DXImageTransform.Microsoft</li>
|
||||
</ul>
|
||||
</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssOldPropertyPrefixes">
|
||||
<div class="label">Old prefixes</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssOldPropertyPrefixes}}
|
||||
<show-offenders modal-title="Old prefixes" metric-name="cssOldPropertyPrefixes" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssOldPropertyPrefixesTooltip = true"></div>
|
||||
<modal-dialog show="cssOldPropertyPrefixesTooltip" dialog-title="Clean up old CSS3 prefixes">
|
||||
<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssUniversalSelectors">
|
||||
<div class="label">Universal selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssUniversalSelectors}}
|
||||
<show-offenders modal-title="Universal selectors" metric-name="cssUniversalSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssUniversalSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssUniversalSelectorsTooltip" dialog-title="Avoid universal selectors">
|
||||
<p>Universal selectors are the most expensive CSS selectors.</p>
|
||||
<p>More informations <a href="http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/" target="_blank">here</a>.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssRedundantBodySelectors">
|
||||
<div class="label">Redundant body selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRedundantBodySelectors}}
|
||||
<show-offenders modal-title="Redundant body selectors" metric-name="cssRedundantBodySelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRedundantBodySelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssRedundantBodySelectorsTooltip" dialog-title="Useless redundant selectors">
|
||||
<p>This is one way to remove complexity from a CSS rule. Generally, when "body" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssRedundantChildNodesSelectors">
|
||||
<div class="label">Redundant tags selectors</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssRedundantChildNodesSelectors}}
|
||||
<show-offenders modal-title="Redundant tags selectors" metric-name="cssRedundantChildNodesSelectors" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cssRedundantChildNodesSelectorsTooltip = true"></div>
|
||||
<modal-dialog show="cssRedundantChildNodesSelectorsTooltip" dialog-title="Useless redundant selectors">
|
||||
<p>Some tags included inside other tags are obvious. For example, when "ul li" is specified in a rule, "ul" can be removed because the "li" element is <b>always</b> inside a "ul". Same thing for "tr td", "select option", ...</p>
|
||||
<p>Lowering compexity in CSS selectors can make the page load a little faster.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.requests">{{notations.requests}}</div>
|
||||
<div class="notation">Requests number</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-class="{'warning': phantomasResults.metrics.requests > 200}">
|
||||
<div class="label">Total requests</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.requests}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="requestsTooltip = true"></div>
|
||||
<modal-dialog show="requestsTooltip" dialog-title="Minimize the number of requests">
|
||||
<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p>
|
||||
<p>There are several technics to reduce their number:
|
||||
<ul>
|
||||
<li>Concatenate JS files</li>
|
||||
<li>Concatenate CSS files</li>
|
||||
<li>Embed or inline small JS or CSS files in the HTML</li>
|
||||
<li>Create sprites or icon fonts</li>
|
||||
<li>Base64 encode small images in HTML or stylesheets</li>
|
||||
<li>Use lazyloading for images</li>
|
||||
</ul>
|
||||
</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.htmlCount">
|
||||
<div class="label">Documents</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.htmlCount}}
|
||||
<show-offenders modal-title="HTML count" metric-name="htmlCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jsCount" ng-class="{'warning': phantomasResults.metrics.jsCount > 40}">
|
||||
<div class="label">Scripts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsCount}}
|
||||
<show-offenders modal-title="JS count" metric-name="jsCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cssCount" ng-class="{'warning': phantomasResults.metrics.cssCount > 25}">
|
||||
<div class="label">Stylesheets</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cssCount}}
|
||||
<show-offenders modal-title="CSS count" metric-name="cssCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.imageCount" ng-class="{'warning': phantomasResults.metrics.imageCount > 80}">
|
||||
<div class="label">Images</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.imageCount}}
|
||||
<show-offenders modal-title="Image count" metric-name="imageCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.webfontCount" ng-class="{'warning': phantomasResults.metrics.webfontCount > 4}">
|
||||
<div class="label">Fonts</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.webfontCount}}
|
||||
<show-offenders modal-title="Webfont count" metric-name="webfontCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.videoCount">
|
||||
<div class="label">Videos</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.videoCount}}
|
||||
<show-offenders modal-title="Video count" metric-name="videoCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.jsonCount">
|
||||
<div class="label">JSON</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.jsonCount}}
|
||||
<show-offenders modal-title="JSON count" metric-name="jsonCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.otherCount">
|
||||
<div class="label">Other</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.otherCount}}
|
||||
<show-offenders modal-title="Other count" metric-name="otherCount" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-class="notations.network">{{notations.network}}</div>
|
||||
<div class="notation">Network</div>
|
||||
<div class="criteria">
|
||||
<div class="table">
|
||||
<div ng-if="phantomasResults.metrics.notFound" class="warning">
|
||||
<div class="label">404 not found</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.notFound}}
|
||||
<show-offenders modal-title="404 not found" metric-name="notFound" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="notFoundTooltip = true"></div>
|
||||
<modal-dialog show="notFoundTooltip" dialog-title="404 errors">
|
||||
<p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.closedConnections" ng-class="{'warning': phantomasResults.metrics.closedConnections > 20}">
|
||||
<div class="label">Connections closed</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.closedConnections}}
|
||||
<show-offenders modal-title="Connections closed" metric-name="closedConnections" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="closedConnectionsTooltip = true"></div>
|
||||
<modal-dialog show="closedConnectionsTooltip" dialog-title="Connection keep-alive is important">
|
||||
<p>This counts the number of requests not keeping the connection alive (specifying "Connection: close" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p>
|
||||
<p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p>
|
||||
<p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.multipleRequests" ng-class="{'warning': phantomasResults.metrics.multipleRequests > 10}">
|
||||
<div class="label">Duplicated requests</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.multipleRequests}}
|
||||
<show-offenders modal-title="Static assets requested more than once" metric-name="multipleRequests" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="multipleRequestsTooltip = true"></div>
|
||||
<modal-dialog show="multipleRequestsTooltip" dialog-title="The same file is requested twice">
|
||||
<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingDisabled" ng-class="{'warning': phantomasResults.metrics.cachingDisabled > 25}">
|
||||
<div class="label">Caching disabled</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingDisabled}}
|
||||
<show-offenders modal-title="Caching disabled" metric-name="cachingDisabled" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingDisabledTooltip = true"></div>
|
||||
<modal-dialog show="cachingDisabledTooltip" dialog-title="Caching disabled">
|
||||
<p>Counts responses with caching disabled (max-age=0)</p>
|
||||
<p>Fix immediatly if on static assets.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingNotSpecified" ng-class="{'warning': phantomasResults.metrics.cachingNotSpecified > 50}">
|
||||
<div class="label">Caching not specified</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingNotSpecified}}
|
||||
<show-offenders modal-title="Caching not specified" metric-name="cachingNotSpecified" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingNotSpecifiedTooltip = true"></div>
|
||||
<modal-dialog show="cachingNotSpecifiedTooltip" dialog-title="No caching header">
|
||||
<p>Responses with no caching header sent (either Cache-Control or Expires).</p>
|
||||
<p>Every request should have a cache time specified. If you really don't want cache, specify "max-age=0", otherwise some browsers will try to cache.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.cachingTooShort">
|
||||
<div class="label">Caching too short</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.cachingTooShort}}
|
||||
<show-offenders modal-title="Caching too short" metric-name="cachingTooShort" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="cachingTooShortTooltip = true"></div>
|
||||
<modal-dialog show="cachingTooShortTooltip" dialog-title="Increase cache time">
|
||||
<p>Responses with too short caching time (less than a week).</p>
|
||||
<p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="phantomasResults.metrics.domains" ng-class="{'warning': phantomasResults.metrics.domains > 40}">
|
||||
<div class="label">Different domains</div>
|
||||
<div class="result">
|
||||
{{phantomasResults.metrics.domains}}
|
||||
<show-offenders modal-title="Different domains" metric-name="domains" phantomas-results="phantomasResults"></show-offenders>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="icon-question" ng-click="domainsTooltip = true"></div>
|
||||
<modal-dialog show="domainsTooltip" dialog-title="Reduce the number of domains">
|
||||
<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p>
|
||||
<p>By the way, domain sharding is not a good practice anymore.</p>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="view == 'execution' && !phantomasResults.error" class="execution">
|
||||
<h2>Javascript Timeline</h2>
|
||||
<p>
|
||||
This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.
|
||||
</p>
|
||||
<div class="timeline">
|
||||
<div class="chart">
|
||||
<div class="chartPoints">
|
||||
<div ng-repeat="duration in timeline track by $index"
|
||||
class="interval"
|
||||
ng-class="{
|
||||
domCreation: $index * timelineIntervalDuration < phantomasResults.metrics.domInteractive,
|
||||
domInteractive: $index * timelineIntervalDuration >= phantomasResults.metrics.domInteractive
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoaded,
|
||||
domContentLoaded: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoaded
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoadedEnd,
|
||||
domContentLoadedEnd: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoadedEnd
|
||||
&& $index * timelineIntervalDuration < phantomasResults.metrics.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= phantomasResults.metrics.domComplete
|
||||
}">
|
||||
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
|
||||
<div class="tooltip detailsOverlay">
|
||||
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime">0 ms</div>
|
||||
<div class="endTime">{{endTime | number: 0}} ms</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="titles">
|
||||
<div class="domCreation"><div class="color"></div>DOM creation</div>
|
||||
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
|
||||
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
|
||||
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
|
||||
<div class="domComplete"><div class="color"></div>Page is complete</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
|
||||
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
|
||||
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
|
||||
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
|
||||
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Javascript Profiler</h2>
|
||||
<p>
|
||||
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
|
||||
</p>
|
||||
<div class="filters">
|
||||
<div>
|
||||
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Show warnings only</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" ng-model="textFilterOn" />
|
||||
Filter by
|
||||
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
|
||||
<toto data-info="mavariable"></toto>
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
<div>Timestamp</div>
|
||||
</div>
|
||||
<div ng-repeat="node in javascript.children"
|
||||
ng-if="(!warningsFilterOn
|
||||
|| (node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5)
|
||||
|| node.data.resultsNumber === 0
|
||||
|| node.data.callDetails.context.length === 0
|
||||
|| node.data.type == 'error'
|
||||
|| node.data.type == 'jQuery version change')
|
||||
&& (!textFilterOn
|
||||
|| !textFilter.length
|
||||
|| node.data.type.indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
|
||||
|| node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
|
||||
ng-class="{
|
||||
showingDetails: node.data.showDetails,
|
||||
jsError: node.data.type == 'error' || node.data.type == 'jQuery version change',
|
||||
windowPerformance: node.data.type == 'domInteractive' || node.data.type == 'domContentLoaded' || node.data.type == 'domContentLoadedEnd' || node.data.type == 'domComplete'
|
||||
}">
|
||||
<div class="index">{{$index + 1}}</div>
|
||||
<div class="type">{{node.data.type}}</div>
|
||||
|
||||
<div class="value">
|
||||
{{node.data.callDetails.arguments[0]}}
|
||||
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div ng-class="{
|
||||
'icon-question': !(node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5) && node.data.resultsNumber !== 0 && node.data.callDetails.context.length !== 0,
|
||||
'icon-warning': node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5 || node.data.resultsNumber === 0 || node.data.callDetails.context.length === 0
|
||||
}"
|
||||
ng-click="onNodeDetailsClick(node)"
|
||||
ng-if="node.data.type != 'jQuery loaded'
|
||||
&& node.data.type != 'jQuery version change'
|
||||
&& node.data.type != 'domInteractive'
|
||||
&& node.data.type != 'domContentLoaded'
|
||||
&& node.data.type != 'domContentLoadedEnd'
|
||||
&& node.data.type != 'domComplete'"></div>
|
||||
<div class="detailsOverlay" ng-show="node.data.showDetails">
|
||||
<div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.domElement">
|
||||
<h4>Called on DOM element</h4>
|
||||
<div>{{node.data.callDetails.context.domElement}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length === 0">
|
||||
<h4>Called on 0 jQuery element</h4>
|
||||
<p class="advice">Useless function call, as the jQuery object is empty.</p>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
|
||||
<h4>Called on 1 jQuery element</h4>
|
||||
<div>{{node.data.callDetails.context.firstElementPath}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length > 1">
|
||||
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
|
||||
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
|
||||
The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
|
||||
</p>
|
||||
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
|
||||
</div>
|
||||
|
||||
<p class="advice" ng-if="node.data.resultsNumber === 0">
|
||||
The query returned 0 results. Could it be unused or dead code?
|
||||
</p>
|
||||
|
||||
<div ng-if="node.data.parsedBacktrace">
|
||||
<h4>Backtrace</h4>
|
||||
<div class="table">
|
||||
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
|
||||
<div>{{trace.fnName || '(anonymous)'}}</div>
|
||||
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
|
||||
</div>
|
||||
<div ng-if="node.data.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
|
||||
<div>can't find any backtrace :/</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.children.length > 0">
|
||||
<h4>Sub processes</h4>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div>Duration</div>
|
||||
</div>
|
||||
<div ng-repeat="node in node.children">
|
||||
<div class="index">{{$index}}</div>
|
||||
<div class="type">{{node.data.type}}</div>
|
||||
<div class="value">
|
||||
{{node.data.callDetails.arguments[0]}}
|
||||
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
|
||||
</div>
|
||||
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms</div>
|
||||
<div class="duration" ng-if="node.data.time == undefined"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
var _phantomas_results = %%RESULTS%%;
|
||||
|
||||
var GA_ID = '%%GA_ID%%';
|
||||
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', GA_ID, 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,96.00C 144.341,96.00, 47.559,161.021,0.00,256.00c 47.559,94.979, 144.341,160.00, 256.00,160.00c 111.657,0.00, 208.439-65.021, 256.00-160.00 C 464.442,161.021, 367.657,96.00, 256.00,96.00z M 382.225,180.852c 30.081,19.187, 55.571,44.887, 74.717,75.148 c-19.146,30.261-44.637,55.961-74.718,75.149C 344.427,355.257, 300.779,368.00, 256.00,368.00c-44.78,0.00-88.428-12.743-126.225-36.852 C 99.695,311.962, 74.205,286.262, 55.058,256.00c 19.146-30.262, 44.637-55.962, 74.717-75.148c 1.959-1.25, 3.938-2.461, 5.929-3.65 C 130.725,190.866, 128.00,205.613, 128.00,221.00c0.00,70.692, 57.308,128.00, 128.00,128.00s 128.00-57.308, 128.00-128.00c0.00-15.387-2.725-30.134-7.704-43.799 C 378.286,178.39, 380.265,179.602, 382.225,180.852z M 256.00,205.00c0.00,26.51-21.49,48.00-48.00,48.00s-48.00-21.49-48.00-48.00s 21.49-48.00, 48.00-48.00 S 256.00,178.49, 256.00,205.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 224.00,384.00A32.00,32.00 2520.00 1,0 288.00,384A32.00,32.00 2520.00 1,0 224.00,384zM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,5 +0,0 @@
|
||||
angular.module('YellowLabTools', [
|
||||
'Results',
|
||||
'ngModal',
|
||||
'ShowOffendersDirective'
|
||||
]);
|
||||
@@ -1,406 +0,0 @@
|
||||
var app = angular.module('Results', []);
|
||||
|
||||
app.controller('ResultsCtrl', function ($scope) {
|
||||
// Grab results from nodeJS served page
|
||||
$scope.phantomasResults = window._phantomas_results;
|
||||
|
||||
$scope.view = 'execution';
|
||||
|
||||
if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
|
||||
|
||||
// Get the execution tree from the offenders
|
||||
$scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
|
||||
|
||||
// Sort globalVariables offenders alphabetically
|
||||
if ($scope.phantomasResults.offenders.globalVariables) {
|
||||
$scope.phantomasResults.offenders.globalVariables.sort();
|
||||
}
|
||||
|
||||
|
||||
initSummaryView();
|
||||
initJSTimelineView();
|
||||
|
||||
}
|
||||
|
||||
$scope.setView = function(viewName) {
|
||||
$scope.view = viewName;
|
||||
};
|
||||
|
||||
$scope.onNodeDetailsClick = function(node) {
|
||||
var isOpen = node.data.showDetails;
|
||||
if (!isOpen) {
|
||||
// Close all other nodes
|
||||
$scope.javascript.children.forEach(function(currentNode) {
|
||||
currentNode.data.showDetails = false;
|
||||
});
|
||||
|
||||
// Parse the backtrace
|
||||
if (!node.data.parsedBacktrace) {
|
||||
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
|
||||
}
|
||||
|
||||
}
|
||||
node.data.showDetails = !isOpen;
|
||||
};
|
||||
|
||||
function initSummaryView() {
|
||||
|
||||
// Read the main elements of the tree and sum the total time
|
||||
$scope.totalJSTime = 0;
|
||||
$scope.inBodyDomManipulations = 0;
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
if (node.data.time) {
|
||||
$scope.totalJSTime += node.data.time;
|
||||
}
|
||||
|
||||
if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
|
||||
node.data.type !== 'jQuery - onDOMReady') {
|
||||
$scope.inBodyDomManipulations ++;
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Grab the notes
|
||||
$scope.notations = {
|
||||
domComplexity: getDomComplexityScore(),
|
||||
jsDomManipulations: getJsDomManipulationsScore(),
|
||||
jsBadPractices: getJSBadPracticesScore(),
|
||||
jQueryLoading: getJQueryLoadingScore(),
|
||||
cssComplexity: getCSSComplexityScore(),
|
||||
badCss: getBadCssScore(),
|
||||
requests: requestsScore(),
|
||||
network: networkScore()
|
||||
};
|
||||
}
|
||||
|
||||
function initJSTimelineView() {
|
||||
|
||||
if (!$scope.javascript.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
$scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
$scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
$scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
$scope.phantomasResults.metrics.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Now read the tree and display it on a timeline
|
||||
|
||||
// Split the timeline into 200 intervals
|
||||
var numberOfIntervals = 199;
|
||||
var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
|
||||
|
||||
// Pre-fill array of as many elements as there are milleseconds
|
||||
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the milliseconds array from the execution tree
|
||||
treeRunner($scope.javascript, function(node) {
|
||||
|
||||
if (node.data.time !== undefined) {
|
||||
|
||||
// Ignore artefacts (durations > 100ms)
|
||||
var time = Math.min(node.data.time, 100) || 1;
|
||||
|
||||
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
|
||||
millisecondsArray[i] |= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.type !== 'main') {
|
||||
// Don't check the children
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill array of 200 elements
|
||||
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the timeline from the milliseconds array
|
||||
millisecondsArray.forEach(function(value, timestamp) {
|
||||
if (value === 1) {
|
||||
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Get the maximum value of the array (needed for display)
|
||||
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
|
||||
}
|
||||
|
||||
|
||||
function getDomComplexityScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.DOMelementsCount +
|
||||
Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
|
||||
$scope.phantomasResults.metrics.iframesCount * 50 +
|
||||
$scope.phantomasResults.metrics.DOMidDuplicated * 25;
|
||||
if (score > 1000) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 1500) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 2000) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 3000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 4000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJsDomManipulationsScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
|
||||
$scope.phantomasResults.metrics.DOMqueries +
|
||||
$scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
|
||||
$scope.phantomasResults.metrics.eventsBound;
|
||||
if (score > 300) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 500) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 700) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 1000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 1400) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJSBadPracticesScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
|
||||
$scope.phantomasResults.metrics.evalCalls * 2 +
|
||||
$scope.phantomasResults.metrics.jsErrors * 10 +
|
||||
$scope.phantomasResults.metrics.consoleMessages / 2 +
|
||||
$scope.phantomasResults.metrics.globalVariables / 20 +
|
||||
Math.sqrt($scope.inBodyDomManipulations);
|
||||
if (score > 10) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 15) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 20) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 30) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 45) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getJQueryLoadingScore() {
|
||||
var note = 'NA';
|
||||
if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
|
||||
note = 'F';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion) {
|
||||
if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
|
||||
note = 'A';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
|
||||
note = 'B';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
|
||||
note = 'C';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
|
||||
note = 'D';
|
||||
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
|
||||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
|
||||
note = 'E';
|
||||
}
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getCSSComplexityScore() {
|
||||
if (!$scope.phantomasResults.metrics.cssRules) {
|
||||
return 'NA';
|
||||
}
|
||||
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.cssRules +
|
||||
$scope.phantomasResults.metrics.cssComplexSelectors * 5 +
|
||||
$scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
|
||||
if (score > 800) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 1200) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 2500) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 4000) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 6000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function getBadCssScore() {
|
||||
if ($scope.phantomasResults.metrics.cssParsingErrors) {
|
||||
return 'F';
|
||||
} else if (!$scope.phantomasResults.metrics.cssRules) {
|
||||
return 'NA';
|
||||
}
|
||||
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
|
||||
$scope.phantomasResults.metrics.cssDuplicatedProperties +
|
||||
$scope.phantomasResults.metrics.cssEmptyRules +
|
||||
$scope.phantomasResults.metrics.cssExpressions * 10 +
|
||||
$scope.phantomasResults.metrics.cssImportants * 2 +
|
||||
$scope.phantomasResults.metrics.cssOldIEFixes * 10 +
|
||||
$scope.phantomasResults.metrics.cssOldPropertyPrefixes +
|
||||
$scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
|
||||
$scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
|
||||
$scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
|
||||
$scope.phantomasResults.metrics.cssImports * 50;
|
||||
if (score > 50) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 200) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 500) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 1000) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function requestsScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.requests;
|
||||
if (score > 30) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 45) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 60) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 80) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
function networkScore() {
|
||||
var note = 'A';
|
||||
var score = $scope.phantomasResults.metrics.notFound * 25 +
|
||||
$scope.phantomasResults.metrics.closedConnections * 10 +
|
||||
$scope.phantomasResults.metrics.multipleRequests * 10 +
|
||||
$scope.phantomasResults.metrics.cachingDisabled * 2 +
|
||||
$scope.phantomasResults.metrics.cachingNotSpecified +
|
||||
$scope.phantomasResults.metrics.cachingTooShort / 2 +
|
||||
$scope.phantomasResults.metrics.domains;
|
||||
if (score > 20) {
|
||||
note = 'B';
|
||||
}
|
||||
if (score > 40) {
|
||||
note = 'C';
|
||||
}
|
||||
if (score > 60) {
|
||||
note = 'D';
|
||||
}
|
||||
if (score > 80) {
|
||||
note = 'E';
|
||||
}
|
||||
if (score > 100) {
|
||||
note = 'F';
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function parseBacktrace(str) {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var splited = str.split(' / ');
|
||||
splited.forEach(function(trace) {
|
||||
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
|
||||
if (result && result[2].length > 0) {
|
||||
var filePath = result[2];
|
||||
var chunks = filePath.split('/');
|
||||
var fileName = chunks[chunks.length - 1];
|
||||
|
||||
out.push({
|
||||
fnName: result[1],
|
||||
fileName: fileName,
|
||||
filePath: filePath,
|
||||
line: result[3]
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Goes on every node of the tree and calls the function fn. If fn returns false on a node, its children won't be checked.
|
||||
function treeRunner(node, fn) {
|
||||
if (fn(node) !== false && node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
treeRunner(child, fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
var app = angular.module("ShowOffendersDirective", []);
|
||||
|
||||
app.directive('showOffenders', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
modalTitle: "@",
|
||||
metricName: "@",
|
||||
phantomasResults: "="
|
||||
},
|
||||
controller: function($scope, $element, $attrs, $location) {
|
||||
$scope.dialogShown = false;
|
||||
},
|
||||
template: ' <span ng-click="dialogShown = true" class="icon-eye" title="See offenders"></span><modal-dialog show="dialogShown" dialog-title="{{modalTitle}}: {{phantomasResults.metrics[metricName]}}" width="70%"><ul><li ng-repeat="offender in phantomasResults.offenders[metricName] track by $index">{{offender}}</li></ul></modal-dialog>'
|
||||
};
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
.launchBtn {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
input[type=submit],
|
||||
input.url {
|
||||
padding: 0 0.5em;
|
||||
margin: 0.5em;
|
||||
font-size: 1.2em;
|
||||
height: 2em;
|
||||
border: 0 solid;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
outline: none;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
input[type=submit].clicked {
|
||||
color: #ddd;
|
||||
position: relative;
|
||||
left: 0.1em;
|
||||
top: 0.2em;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
@-webkit-keyframes rotating {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.waiting .icon-lab {
|
||||
-webkit-animation: rotating 3s linear 0s infinite;
|
||||
-webkit-transform: translateZ(0);
|
||||
animation: rotating 3s linear 0s infinite;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
@eye-font-family: "fontsmith-icons";
|
||||
@eye-value: "\e004";
|
||||
@eye: '"fontsmith-icons"' '"\\e004"';
|
||||
@lab-font-family: "fontsmith-icons";
|
||||
@lab-value: "\e003";
|
||||
@lab: '"fontsmith-icons"' '"\\e003"';
|
||||
@bars-font-family: "fontsmith-icons";
|
||||
@bars-value: "\e005";
|
||||
@bars: '"fontsmith-icons"' '"\\e005"';
|
||||
@warning-font-family: "fontsmith-icons";
|
||||
@warning-value: "\e000";
|
||||
@warning: '"fontsmith-icons"' '"\\e000"';
|
||||
@arrow-left3-font-family: "fontsmith-icons";
|
||||
@arrow-left3-value: "\e006";
|
||||
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
|
||||
@question-font-family: "fontsmith-icons";
|
||||
@question-value: "\e001";
|
||||
@question: '"fontsmith-icons"' '"\\e001"';
|
||||
@list-font-family: "fontsmith-icons";
|
||||
@list-value: "\e002";
|
||||
@list: '"fontsmith-icons"' '"\\e002"';
|
||||
|
||||
.icon-font-family(@char) {
|
||||
font-family: ~`@{char}[0]`;
|
||||
}
|
||||
|
||||
.icon-font() {
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.icon-content(@char) {
|
||||
content: ~`@{char}[1]`;
|
||||
}
|
||||
|
||||
.icon(@char) {
|
||||
.icon-font-family(@char);
|
||||
.icon-font();
|
||||
|
||||
&:before {
|
||||
.icon-content(@char);
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "fontsmith-icons";
|
||||
src:url("/public/fonts/icons.woff") format("woff"),
|
||||
;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.launchBtn {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input[type=submit], input.url {
|
||||
padding: 0 0.5em;
|
||||
margin: 0.5em;
|
||||
font-size: 1.2em;
|
||||
height: 2em;
|
||||
border: 0 solid;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
outline: none;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
input[type=submit].clicked {
|
||||
color: #ddd;
|
||||
position: relative;
|
||||
left: 0.1em;
|
||||
top: 0.2em;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotating {
|
||||
from { -webkit-transform: rotate(0deg); }
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.waiting .icon-lab {
|
||||
-webkit-animation: rotating 3s linear 0s infinite;
|
||||
-webkit-transform: translateZ(0);
|
||||
animation: rotating 3s linear 0s infinite;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
@import "icons.less";
|
||||
|
||||
html {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
background: #9c4274;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
|
||||
font-family: 'Century Gothic', helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #4e1836;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.star {
|
||||
font-weight: bold;
|
||||
span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.icon-lab {
|
||||
.icon(@lab);
|
||||
}
|
||||
.icon-question {
|
||||
.icon(@question);
|
||||
}
|
||||
.icon-warning {
|
||||
.icon(@warning);
|
||||
}
|
||||
.icon-back {
|
||||
.icon(@arrow-left3);
|
||||
}
|
||||
.icon-summary {
|
||||
.icon(@list);
|
||||
}
|
||||
.icon-spaghetti {
|
||||
.icon(@bars);
|
||||
}
|
||||
.icon-eye {
|
||||
.icon(@eye);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "fontsmith-icons";
|
||||
src: url("/public/fonts/icons.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
html {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
background: #9c4274;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
body,
|
||||
input[type=submit],
|
||||
input[type=text],
|
||||
input[type=url],
|
||||
input[type=number],
|
||||
button {
|
||||
font-family: 'Century Gothic', helvetica, arial, sans-serif;
|
||||
}
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
h1 span {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #ffa319;
|
||||
}
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #4e1836;
|
||||
}
|
||||
.footer a {
|
||||
color: inherit;
|
||||
}
|
||||
.footer .star {
|
||||
font-weight: bold;
|
||||
}
|
||||
.footer .star span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
/* Icons */
|
||||
.icon-lab {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-lab:before {
|
||||
content: "\e003";
|
||||
}
|
||||
.icon-question {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-question:before {
|
||||
content: "\e001";
|
||||
}
|
||||
.icon-warning {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-warning:before {
|
||||
content: "\e000";
|
||||
}
|
||||
.icon-back {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-back:before {
|
||||
content: "\e006";
|
||||
}
|
||||
.icon-summary {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-summary:before {
|
||||
content: "\e002";
|
||||
}
|
||||
.icon-spaghetti {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-spaghetti:before {
|
||||
content: "\e005";
|
||||
}
|
||||
.icon-eye {
|
||||
font-family: "fontsmith-icons";
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "\e004";
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
/* Timeline colors, related to Window Performances */
|
||||
.resultsMenu {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.resultsMenu .menuItem {
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
width: 8em;
|
||||
height: 7em;
|
||||
color: #fff;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.resultsMenu .back {
|
||||
color: #5e2846;
|
||||
border-color: #5e2846;
|
||||
}
|
||||
.resultsMenu .menuItem div {
|
||||
padding-top: 0.5em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.resultsMenu .active,
|
||||
.resultsMenu .menuItem.active:hover {
|
||||
color: #ffa319;
|
||||
border-color: #ffa319;
|
||||
}
|
||||
.resultsMenu .menuItem:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.resultsMenu span {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
h4 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.summary,
|
||||
.metrics,
|
||||
.execution {
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
.notations {
|
||||
display: table;
|
||||
width: 90%;
|
||||
margin: 0 10%;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
.notations > div {
|
||||
display: table-row;
|
||||
}
|
||||
.notations > div > div {
|
||||
display: table-cell;
|
||||
height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notations .notation {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria {
|
||||
font-weight: normal;
|
||||
}
|
||||
.notations .A,
|
||||
.notations .B,
|
||||
.notations .C,
|
||||
.notations .D,
|
||||
.notations .E,
|
||||
.notations .F,
|
||||
.notations .NA {
|
||||
width: 2.5em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.notations .A {
|
||||
/* green */
|
||||
background: #00DB61;
|
||||
}
|
||||
.notations .B {
|
||||
/* green */
|
||||
background: #CAD63D;
|
||||
}
|
||||
.notations .C {
|
||||
/* yellow */
|
||||
background: #FFD119;
|
||||
}
|
||||
.notations .D {
|
||||
/* orange */
|
||||
background: #FFA319;
|
||||
}
|
||||
.notations .E {
|
||||
/* red */
|
||||
background: #FF6600;
|
||||
}
|
||||
.notations .F {
|
||||
/* red */
|
||||
background: #FF1919;
|
||||
}
|
||||
.notations .NA {
|
||||
/* Non applicable */
|
||||
background: #CCC;
|
||||
}
|
||||
.notations .icon-eye {
|
||||
color: #9c4274;
|
||||
cursor: pointer;
|
||||
}
|
||||
.notations .criteria .table {
|
||||
width: 75%;
|
||||
}
|
||||
.notations .criteria .label {
|
||||
width: 70%;
|
||||
}
|
||||
.notations .criteria .result {
|
||||
width: 20%;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .warning .label,
|
||||
.notations .warning .result {
|
||||
color: #FF1919;
|
||||
}
|
||||
.notations .criteria .info {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline {
|
||||
margin: 2em 0 5em;
|
||||
}
|
||||
.timeline .chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.timeline .startTime,
|
||||
.timeline .endTime {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.timeline .startTime {
|
||||
left: 0em;
|
||||
}
|
||||
.timeline .endTime {
|
||||
right: 0em;
|
||||
}
|
||||
.timeline .chartPoints {
|
||||
display: table;
|
||||
height: 100px;
|
||||
width: 99%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeline .interval {
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
height: 100px;
|
||||
width: 0.5%;
|
||||
}
|
||||
.timeline .interval .color {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
.timeline .interval:hover .color {
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: #c2a3ff;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: #7ecccc;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: #a7e846;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: #fffccc;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: #ffe433;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: #ff6600;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: auto;
|
||||
padding: 0.5em 1em;
|
||||
top: -1.5em;
|
||||
right: 1em;
|
||||
}
|
||||
.timeline .interval:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
.timeline .legend {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .legend > div {
|
||||
display: table-row;
|
||||
}
|
||||
.timeline .legend > div > div {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .titles {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timeline .titles > div {
|
||||
padding: 0 1em 0 2em;
|
||||
}
|
||||
.timeline .tips {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.timeline .tips > div {
|
||||
padding: 1em 1em 0 0;
|
||||
}
|
||||
.timeline .legend .color {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
.metrics h4 {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.metrics .module {
|
||||
padding-left: 4em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.metrics .legend {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
}
|
||||
.metrics .offenders {
|
||||
padding-left: 0em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.metrics .offenders div {
|
||||
cursor: pointer;
|
||||
}
|
||||
.metrics .offenders ul {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.filters {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
border: 1px dotted #aaa;
|
||||
}
|
||||
.slowRequestsLimit {
|
||||
width: 3em;
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
input.textFilter {
|
||||
box-shadow: none;
|
||||
font-size: 1em;
|
||||
padding: 0 0.2em;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: none;
|
||||
width: 15em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
.table > div {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.table > div.jsError > .type,
|
||||
.table > div.jsError > .value {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.table > div.windowPerformance > div,
|
||||
.table > div.windowPerformance > div.startTime {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table > div.showingDetails > div {
|
||||
background: #f1c40f;
|
||||
}
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
}
|
||||
.table > div > .type {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .details {
|
||||
position: relative;
|
||||
}
|
||||
.table .details .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table .details .icon-warning {
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay {
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
width: 45em;
|
||||
min-height: 1em;
|
||||
padding: 0 1em 1em;
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay .advice {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.detailsOverlay .trace {
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .duration,
|
||||
.table > div > .startTime {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: #fffccc;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.table .icon-warning {
|
||||
color: #e74c3c;
|
||||
}
|
||||
/**** NgModal popin (have a look inside bower_components) ****/
|
||||
.ng-modal {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.ng-modal-overlay {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ng-modal-dialog {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border: 3px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
.ng-modal-dialog-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-wrap: break-word;
|
||||
max-height: 20em;
|
||||
font-weight: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
.ng-modal-close {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.ng-modal-close-x {
|
||||
font-weight: bold;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.ng-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: solid 1px #999;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
var EasyXml = require('easyxml');
|
||||
|
||||
var ylt = require('../lib/index');
|
||||
|
||||
var cli = meow({
|
||||
help: [
|
||||
'Usage',
|
||||
' yellowlabtools <url> <options>',
|
||||
'',
|
||||
'Options:',
|
||||
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
|
||||
' --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.',
|
||||
' --cookie Adds a cookie on the main domain.',
|
||||
' --auth-user Basic HTTP authentication username.',
|
||||
' --auth-pass Basic HTTP authentication password.',
|
||||
' --reporter The output format: "json" or "xml". Default is "json".',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: '../package.json'
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Check parameters
|
||||
if (cli.input.length < 1) {
|
||||
cli.showHelp();
|
||||
}
|
||||
|
||||
var url = cli.input[0];
|
||||
|
||||
var options = {};
|
||||
|
||||
// Screenshot option
|
||||
var screenshot = cli.flags.screenshot;
|
||||
if (screenshot && (typeof screenshot !== 'string' || screenshot.toLowerCase().indexOf('.png', screenshot.length - 4) === -1)) {
|
||||
console.error('Incorrect parameters: screenshot must be a path that ends with ".png"');
|
||||
process.exit(1);
|
||||
}
|
||||
if (screenshot) {
|
||||
if (path.resolve(screenshot) !== path.normalize(screenshot)) {
|
||||
// It is not an absolute path, so it is relative to the current command-line directory
|
||||
screenshot = path.join(process.cwd(), screenshot);
|
||||
}
|
||||
options.screenshot = cli.flags.screenshot;
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'desktop';
|
||||
|
||||
// Wait for CSS selector
|
||||
options.waitForSelector = cli.flags.waitForSelector || null;
|
||||
|
||||
// Cookie
|
||||
options.cookie = cli.flags.cookie || null;
|
||||
|
||||
// HTTP basic auth
|
||||
options.authUser = cli.flags.authUser || null;
|
||||
options.authPass = cli.flags.authPass || 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) {
|
||||
'use strict';
|
||||
|
||||
ylt(url, options).
|
||||
|
||||
then(function(data) {
|
||||
|
||||
debug('Success');
|
||||
switch(cli.flags.reporter) {
|
||||
case 'xml':
|
||||
var serializer = new EasyXml();
|
||||
console.log(serializer.render(data));
|
||||
break;
|
||||
default:
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
}).fail(function(err) {
|
||||
|
||||
debug('Test failed for %s', url);
|
||||
console.error(err);
|
||||
|
||||
});
|
||||
|
||||
debug('Test launched...');
|
||||
|
||||
})(url, options);
|
||||
@@ -0,0 +1,49 @@
|
||||
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');
|
||||
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
|
||||
|
||||
|
||||
// Middlewares
|
||||
app.use(compress());
|
||||
app.use(bodyParser.json());
|
||||
app.use(cors());
|
||||
app.use(wwwRedirectMiddleware);
|
||||
app.use(authMiddleware);
|
||||
app.use(apiLimitsMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
|
||||
|
||||
// EJS HTML engine
|
||||
app.engine('.html', require('ejs').__express);
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
|
||||
// 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');
|
||||
|
||||
app.locals.baseUrl = settings.baseUrl;
|
||||
|
||||
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,7 +0,0 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"dependencies": {
|
||||
"angular": "~1.3.1",
|
||||
"ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,10 @@
|
||||
.about {
|
||||
margin: 3em auto;
|
||||
width: 50%;
|
||||
}
|
||||
.about p {
|
||||
margin: 2em;
|
||||
}
|
||||
.about a {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
.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 .globalScore .screenshotWrapper.phone:hover:after {
|
||||
top: 1.7em;
|
||||
left: 0.64em;
|
||||
}
|
||||
.summary .globalScore .screenshotWrapper.tablet:hover:after {
|
||||
top: 1.5em;
|
||||
left: 0.9em;
|
||||
}
|
||||
.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 > a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.summary .notations .criteria .table > a:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.summary .notations .criteria .table > a:hover > div.info {
|
||||
background: #FFF;
|
||||
}
|
||||
.summary .notations .criteria .table > a: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;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
.promess {
|
||||
padding: 0em 2em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.price {
|
||||
padding: 0em 2em 3em;
|
||||
margin-top: 0em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
.launchBtn {
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
}
|
||||
.launchBtn:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.launchBtn.disabled {
|
||||
background: #deaca6;
|
||||
}
|
||||
.launchBtn.disabled:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.settings input,
|
||||
.settings select {
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text],
|
||||
.settings input[type=password],
|
||||
.settings textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
.device {
|
||||
margin-top: 3em;
|
||||
}
|
||||
.device .item {
|
||||
display: inline-block;
|
||||
margin: 1em 0.75em;
|
||||
width: 5.5em;
|
||||
height: 5.5em;
|
||||
color: #FFF;
|
||||
border: 1px solid #FFF;
|
||||
padding: 1px;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.device .item.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
.device .item:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.device .item > div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
}
|
||||
.settingsTooltip span {
|
||||
font-size: 0.8em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.settingsTooltip div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 0.5em;
|
||||
width: 25em;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
font-size: 0.8em;
|
||||
border-radius: 1em;
|
||||
border: 2px solid #ffa319;
|
||||
white-space: normal;
|
||||
z-index: 2;
|
||||
}
|
||||
.settingsTooltip:hover div {
|
||||
display: block;
|
||||
}
|
||||
.showAdvanced {
|
||||
display: inline-block;
|
||||
margin-top: 2em;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.showAdvanced:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.currentSettings {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.currentSettings span {
|
||||
color: #ffa319;
|
||||
}
|
||||
.currentSettings span:after {
|
||||
color: #FFF;
|
||||
content: ",";
|
||||
}
|
||||
.currentSettings span:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
.advanced {
|
||||
margin: 1em 0 0;
|
||||
display: table;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-spacing: 0.75em;
|
||||
}
|
||||
.advanced > div {
|
||||
display: table-row;
|
||||
}
|
||||
.advanced > div > div {
|
||||
display: table-cell;
|
||||
width: 75%;
|
||||
}
|
||||
.advanced > div > div.label {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.advanced .subTable {
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.advanced .subTable > div {
|
||||
display: table-row;
|
||||
}
|
||||
.advanced .subTable > div > div {
|
||||
display: table-cell;
|
||||
padding: 0 0 0.75em;
|
||||
}
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 6em 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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.statusSubMessage {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
.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 h3 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
.rule .okThreshold {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.rule .message {
|
||||
width: 80%;
|
||||
margin: 1.5em 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;
|
||||
}
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
.offendersTable .offenderButton,
|
||||
.value .offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.offendersTable .offenderButton.opens,
|
||||
.value .offenderButton.opens {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:after,
|
||||
.value .offenderButton.opens:after {
|
||||
position: relative;
|
||||
left: 0.5em;
|
||||
content: '\25BC';
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.offendersTable .offenderButton > div,
|
||||
.value .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;
|
||||
z-index: 2;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree,
|
||||
.value .offenderButton .domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree > div,
|
||||
.value .offenderButton .domTree > div {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree > div div,
|
||||
.value .offenderButton .domTree > div div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.offendersTable .offenderButton .backtrace,
|
||||
.value .offenderButton .backtrace,
|
||||
.offendersTable .offenderButton .cssFileAndLine,
|
||||
.value .offenderButton .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:hover,
|
||||
.value .offenderButton.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:hover > div,
|
||||
.value .offenderButton.opens:hover > div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.offendersTable .smallerOffenders,
|
||||
.value .smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.checker {
|
||||
/* 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;
|
||||
}
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
}
|
||||
.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;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
}
|
||||
.similarColors > div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
.totalWeightPie {
|
||||
max-width: 39em;
|
||||
margin: 2em auto 4em;
|
||||
}
|
||||
.totalWeightPie canvas {
|
||||
max-width: inherit;
|
||||
}
|
||||
.hugeFile {
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.imageOffenders {
|
||||
display: table;
|
||||
border-spacing: 3em;
|
||||
width: 90%;
|
||||
}
|
||||
.imageOffenders > div {
|
||||
display: table-row;
|
||||
}
|
||||
.imageOffenders > div > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.imageOffenders img {
|
||||
max-height: 10em;
|
||||
max-width: 40em;
|
||||
border: 1px solid #000;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.smallPreview {
|
||||
display: block;
|
||||
max-height: 4em;
|
||||
max-width: 8em;
|
||||
border: 1px solid #000;
|
||||
margin: 1em auto 0.2em;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/* Timeline colors, related to Window Performances */
|
||||
.execution {
|
||||
text-align: center;
|
||||
}
|
||||
.selectScript {
|
||||
padding-bottom: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.selectScript select {
|
||||
max-width: 30em;
|
||||
}
|
||||
.selectScript.empty {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.selectScript.empty select {
|
||||
width: 10em;
|
||||
}
|
||||
.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 .interval .color.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
.table > div,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div,
|
||||
.table > a > 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.highlight > div.startTime {
|
||||
background-color: #C0F090;
|
||||
}
|
||||
.table > div.highlight-remove {
|
||||
transition: 3s;
|
||||
}
|
||||
.table > div.highlight-remove > div.startTime {
|
||||
transition: background-color 3s ease-in;
|
||||
}
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
word-break: normal;
|
||||
}
|
||||
.table > div > .type {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table .children {
|
||||
margin-top: 0.2em;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
.table .child {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.table .child > .child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.table .child:before {
|
||||
content: "↳";
|
||||
}
|
||||
.table .child .childArgs {
|
||||
display: none;
|
||||
}
|
||||
.table .child span {
|
||||
position: relative;
|
||||
}
|
||||
.table .child span:hover {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table .child span:hover div {
|
||||
display: inline-block;
|
||||
}
|
||||
.table .child span:hover .childArgs {
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: 0 1em 0 2em;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background: #EBD8E2;
|
||||
line-height: 1.3em;
|
||||
height: 1.3em;
|
||||
z-index: 2;
|
||||
}
|
||||
.table .showingDetails .child span:hover {
|
||||
background: inherit;
|
||||
}
|
||||
.table .showingDetails .child span:hover .childArgs {
|
||||
display: none;
|
||||
}
|
||||
.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 {
|
||||
display: none;
|
||||
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: 2;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.showDetails .detailsOverlay {
|
||||
display: block;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.queryWithoutResultsFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.queryWithoutResultsFilterOn > div.queryWithoutResults {
|
||||
display: table-row;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
|
||||
display: table-row;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div.eventNotDelegated {
|
||||
display: table-row;
|
||||
}
|
||||
.jsErrorFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jsErrorFilterOn > div.jsError {
|
||||
display: table-row;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 634 B |
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 792 B |
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 679 B |
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 875 B |
@@ -0,0 +1 @@
|
||||
<?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"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 851 B |
@@ -0,0 +1 @@
|
||||
<?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"><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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<?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"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 584 B |
@@ -0,0 +1 @@
|
||||
<?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"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 774 B |
@@ -0,0 +1 @@
|
||||
<?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"><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 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><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 887 B |
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,88 @@
|
||||
var yltApp = angular.module('YellowLabTools', [
|
||||
'ngRoute',
|
||||
'ngSanitize',
|
||||
'ngAnimate',
|
||||
'indexCtrl',
|
||||
'dashboardCtrl',
|
||||
'queueCtrl',
|
||||
'ruleCtrl',
|
||||
'screenshotCtrl',
|
||||
'timelineCtrl',
|
||||
'runsFactory',
|
||||
'resultsFactory',
|
||||
'apiService',
|
||||
'menuService',
|
||||
'settingsService',
|
||||
'gradeDirective',
|
||||
'offendersDirectives',
|
||||
'LocalStorageModule'
|
||||
]);
|
||||
|
||||
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
|
||||
$rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
|
||||
$rootScope.loadedRunId = null;
|
||||
|
||||
var oldHash;
|
||||
|
||||
// We don't want the hash to be kept between two pages
|
||||
$rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
|
||||
var newHash = $location.hash();
|
||||
if (newHash === oldHash) {
|
||||
$location.hash(null);
|
||||
}
|
||||
oldHash = newHash;
|
||||
});
|
||||
|
||||
// Google Analytics
|
||||
$rootScope.$on('$routeChangeSuccess', function(){
|
||||
ga('send', 'pageview', {'page': $location.path()});
|
||||
});
|
||||
|
||||
// GitHub star button (asynchronously loaded iframe)
|
||||
window.addEventListener('load', function() {
|
||||
window.document.getElementById('ghbtn').src = 'http://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large';
|
||||
});
|
||||
}]);
|
||||
|
||||
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);
|
||||
}
|
||||
]);
|
||||
|
||||
// Disable debugging https://docs.angularjs.org/guide/production
|
||||
yltApp.config(['$compileProvider', function ($compileProvider) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}]);
|
||||
@@ -0,0 +1,60 @@
|
||||
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, exclude: 'toolsResults'}, 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 = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
|
||||
}
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
// 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?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
|
||||
};
|
||||
|
||||
$scope.shareOnLinkedin = function(message) {
|
||||
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&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);
|
||||
}
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -0,0 +1,13 @@
|
||||
var indexCtrl = angular.module('indexCtrl', []);
|
||||
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
|
||||
|
||||
$scope.settings = Settings.getMergedSettings();
|
||||
|
||||
$scope.launchTest = function() {
|
||||
if ($scope.url) {
|
||||
Settings.saveSettings($scope.settings);
|
||||
API.launchTest($scope.url, $scope.settings);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,42 @@
|
||||
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();
|
||||
}]);
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
|
||||
|
||||
ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
|
||||
// Configure all charts
|
||||
ChartJsProvider.setOptions({
|
||||
animation: false,
|
||||
colours: ['#FF5252', '#FF8A80'],
|
||||
responsive: true
|
||||
});
|
||||
}]);
|
||||
|
||||
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, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
$scope.rule = $scope.result.rules[$scope.policyName];
|
||||
|
||||
// Init "Total Weight" chart
|
||||
if ($scope.policyName === 'totalWeight') {
|
||||
$scope.weightLabels = [];
|
||||
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
|
||||
$scope.weightData = [];
|
||||
|
||||
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
|
||||
types.forEach(function(type) {
|
||||
$scope.weightLabels.push(type);
|
||||
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
|
||||
});
|
||||
|
||||
$scope.weightOptions = {
|
||||
tooltipTemplate: '<%=label%>: <%=value%> KB'
|
||||
};
|
||||
}
|
||||
|
||||
// Init "Breakpoints" chart
|
||||
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
|
||||
|
||||
// Seek for the biggest breakpoint
|
||||
var max = 0;
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
if (offender.pixels > max) {
|
||||
max = offender.pixels;
|
||||
}
|
||||
});
|
||||
max = Math.max(max + 100, 1400);
|
||||
|
||||
// We group offenders 10px by 10px
|
||||
var GROUP_SIZE = 20;
|
||||
|
||||
// Generate an empty array of values
|
||||
$scope.breakpointsLabels = [];
|
||||
$scope.breakpointsData = [[]];
|
||||
for (var i = 0; i <= max / GROUP_SIZE; i++) {
|
||||
$scope.breakpointsLabels[i] = '';
|
||||
$scope.breakpointsData[0][i] = 0;
|
||||
}
|
||||
|
||||
// Fill it with results
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
|
||||
|
||||
if ($scope.breakpointsLabels[group] !== '') {
|
||||
$scope.breakpointsLabels[group] += '/';
|
||||
}
|
||||
$scope.breakpointsLabels[group] += offender.breakpoint;
|
||||
|
||||
$scope.breakpointsData[0][group] += offender.count;
|
||||
});
|
||||
|
||||
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
|
||||
$scope.breakpointsColours = ['#9c4274'];
|
||||
$scope.breakpointsOptions = {
|
||||
scaleShowGridLines: false,
|
||||
barShowStroke: false,
|
||||
showTooltips: false,
|
||||
pointDot: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
strokeColor: 'rgba(20, 200, 20, 1)',
|
||||
scaleFontSize: 9
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -0,0 +1,30 @@
|
||||
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, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
}, function(err) {
|
||||
$scope.error = true;
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -0,0 +1,190 @@
|
||||
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, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
render();
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
initFilters();
|
||||
initScriptFiltering();
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
$timeout(initProfiler, 100);
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
var hash = $location.hash();
|
||||
var filter = null;
|
||||
|
||||
if (hash.indexOf('filter=') === 0) {
|
||||
filter = hash.substr(7);
|
||||
}
|
||||
|
||||
$scope.warningsFilterOn = (filter !== null);
|
||||
$scope.warningsFilters = {
|
||||
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
|
||||
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
|
||||
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
|
||||
jsError: (filter === null || filter === 'jsError')
|
||||
};
|
||||
}
|
||||
|
||||
function initScriptFiltering() {
|
||||
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
|
||||
$scope.scripts = [];
|
||||
|
||||
offenders.forEach(function(filePath) {
|
||||
var shortPath = filePath;
|
||||
|
||||
if (filePath.length > 100) {
|
||||
shortPath = filePath.substr(0, 98) + '...';
|
||||
}
|
||||
|
||||
var scriptObj = {
|
||||
fullPath: filePath,
|
||||
shortPath: shortPath
|
||||
};
|
||||
|
||||
$scope.scripts.push(scriptObj);
|
||||
});
|
||||
}
|
||||
|
||||
function initExecutionTree() {
|
||||
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
|
||||
|
||||
// Detect the last event of all (before filtering) and read time
|
||||
var lastEvent = originalExecutions[originalExecutions.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
|
||||
// Filter
|
||||
$scope.executionTree = [];
|
||||
originalExecutions.forEach(function(node) {
|
||||
|
||||
// Filter by script (if enabled)
|
||||
if ($scope.selectedScript) {
|
||||
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
|
||||
return;
|
||||
}
|
||||
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.executionTree.push(node);
|
||||
});
|
||||
}
|
||||
|
||||
function initTimeline() {
|
||||
|
||||
// Split the timeline into 200 intervals
|
||||
var numberOfIntervals = 199;
|
||||
$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;
|
||||
}
|
||||
|
||||
$scope.changeScript = function() {
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
initProfiler();
|
||||
};
|
||||
|
||||
$scope.findLineIndexByTimestamp = function(timestamp) {
|
||||
var lineIndex = 0;
|
||||
|
||||
for (var i = 0; i < $scope.executionTree.length; i ++) {
|
||||
var delta = $scope.executionTree[i].data.timestamp - timestamp;
|
||||
|
||||
if (delta < $scope.timelineIntervalDuration) {
|
||||
lineIndex = i;
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lineIndex;
|
||||
};
|
||||
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
}]);
|
||||
|
||||
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attributes) {
|
||||
// When the user clicks on the timeline, find the right profiler line and scroll to it
|
||||
element.on('click', function() {
|
||||
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
|
||||
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
|
||||
|
||||
// Animate the background color to "flash" the row
|
||||
lineElement.addClass('highlight');
|
||||
$timeout(function() {
|
||||
$animate.removeClass(lineElement, 'highlight');
|
||||
scope.$digest();
|
||||
}, 50);
|
||||
|
||||
|
||||
window.scrollTo(0, lineElement[0].offsetTop);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,33 @@
|
||||
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';
|
||||
};
|
||||
}]
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,921 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
var offendersDirectives = angular.module('offendersDirectives', []);
|
||||
|
||||
function getdomTreeHTML(tree) {
|
||||
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
|
||||
}
|
||||
|
||||
function getdomTreeInnerHTML(tree) {
|
||||
return recursiveHtmlBuilder(tree);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
offendersDirectives.directive('domTree', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
tree: '='
|
||||
},
|
||||
template: '<div class="domTree"></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
element.append(getdomTreeInnerHTML(scope.tree));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function getDomElementButtonHTML(obj, onASingleLine) {
|
||||
if (obj.tree && !onASingleLine) {
|
||||
return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
|
||||
} else {
|
||||
return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function getDomElementButtonInnerHTML(obj, onASingleLine) {
|
||||
if (obj.type === 'html' ||
|
||||
obj.type === 'body' ||
|
||||
obj.type === 'head' ||
|
||||
obj.type === 'window' ||
|
||||
obj.type === 'document' ||
|
||||
obj.type === 'fragment') {
|
||||
return obj.type;
|
||||
}
|
||||
|
||||
if (obj.type === 'notAnElement') {
|
||||
return 'Incorrect element';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
if (obj.type === 'domElement') {
|
||||
html = 'DOM element <b>' + obj.element + '</b>';
|
||||
} else if (obj.type === 'fragmentElement') {
|
||||
html = 'Fragment element <b>' + obj.element + '</b>';
|
||||
} else if (obj.type === 'createdElement') {
|
||||
html = 'Created element <b>' + obj.element + '</b>';
|
||||
}
|
||||
|
||||
if (obj.tree && !onASingleLine) {
|
||||
html += getdomTreeHTML(obj.tree);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
offendersDirectives.directive('domElementButton', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
obj: '='
|
||||
},
|
||||
template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
element.append(getDomElementButtonInnerHTML(scope.obj));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function getJQueryContextButtonHTML(context, onASingleLine) {
|
||||
if (context.length === 0) {
|
||||
return '<span class="offenderButton">Empty jQuery object</span>';
|
||||
}
|
||||
|
||||
if (context.length === 1) {
|
||||
return getDomElementButtonHTML(context.elements[0], onASingleLine);
|
||||
}
|
||||
|
||||
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
|
||||
if (context.length === 3) {
|
||||
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
|
||||
} else if (context.length > 3) {
|
||||
html += ' and ' + (context.length - 2) + ' more...';
|
||||
}
|
||||
return html + ')';
|
||||
}
|
||||
|
||||
function isJQuery(node) {
|
||||
return node.data.type.indexOf('jQuery ') === 0;
|
||||
}
|
||||
|
||||
function getNonJQueryHTML(node, onASingleLine) {
|
||||
var type = node.data.type;
|
||||
|
||||
if (node.windowPerformance) {
|
||||
switch (type) {
|
||||
case 'documentScroll':
|
||||
return '(triggering the scroll event on <b>document</b>)';
|
||||
|
||||
case 'windowScroll':
|
||||
return '(triggering the scroll event on <b>window</b>)';
|
||||
|
||||
case 'window.onscroll':
|
||||
return '(calling the <b>window.onscroll</b> function)';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.data.callDetails) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var args = node.data.callDetails.arguments;
|
||||
var ctxt = node.data.callDetails.context;
|
||||
|
||||
|
||||
switch (type) {
|
||||
case 'getElementById':
|
||||
case 'createElement':
|
||||
return '<b>' + args[0] + '</b>';
|
||||
|
||||
case 'getElementsByClassName':
|
||||
case 'getElementsByTagName':
|
||||
case 'querySelector':
|
||||
case 'querySelectorAll':
|
||||
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'appendChild':
|
||||
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'insertBefore':
|
||||
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
|
||||
|
||||
case 'addEventListener':
|
||||
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'getComputedStyle':
|
||||
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
|
||||
|
||||
case 'error':
|
||||
return args[0];
|
||||
|
||||
case 'jQuery - onDOMReady':
|
||||
return '(function)';
|
||||
|
||||
case 'documentScroll':
|
||||
return 'The scroll event just triggered on document';
|
||||
|
||||
case 'windowScroll':
|
||||
return 'The scroll event just triggered on window';
|
||||
|
||||
case 'window.onscroll':
|
||||
return 'The window.onscroll function just got called';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getJQueryHTML(node, onASingleLine) {
|
||||
var type = node.data.type;
|
||||
var unescapedArgs = node.data.callDetails.arguments;
|
||||
var args = [];
|
||||
var ctxt = node.data.callDetails.context;
|
||||
|
||||
// escape HTML in args
|
||||
for (var i = 0 ; i < 4 ; i ++) {
|
||||
if (unescapedArgs[i] !== undefined) {
|
||||
args[i] = escapeHTML(unescapedArgs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'jQuery loaded' || type === 'jQuery version change') {
|
||||
return args[0];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'jQuery - onDOMReady':
|
||||
case 'jQuery - windowOnLoad':
|
||||
return '(function)';
|
||||
|
||||
case 'jQuery - Sizzle call':
|
||||
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'jQuery - find':
|
||||
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
|
||||
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return '<b>' + args[0] + '</b>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - html':
|
||||
if (args[0] !== undefined) {
|
||||
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - append':
|
||||
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - appendTo':
|
||||
return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - prepend':
|
||||
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - prependTo':
|
||||
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - before':
|
||||
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - insertBefore':
|
||||
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - after':
|
||||
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - insertAfter':
|
||||
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - remove':
|
||||
case 'jQuery - detach':
|
||||
if (args[0]) {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - empty':
|
||||
case 'jQuery - clone':
|
||||
case 'jQuery - unwrap':
|
||||
case 'jQuery - show':
|
||||
case 'jQuery - hide':
|
||||
case 'jQuery - animate':
|
||||
case 'jQuery - fadeIn':
|
||||
case 'jQuery - fadeOut':
|
||||
case 'jQuery - fadeTo':
|
||||
case 'jQuery - fadeToggle':
|
||||
case 'jQuery - slideDown':
|
||||
case 'jQuery - slideUp':
|
||||
case 'jQuery - slideToggle':
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - replaceWith':
|
||||
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - replaceAll':
|
||||
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - text':
|
||||
if (args[0] !== undefined) {
|
||||
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - wrap':
|
||||
case 'jQuery - wrapAll':
|
||||
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - wrapInner':
|
||||
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - css':
|
||||
case 'jQuery - attr':
|
||||
case 'jQuery - prop':
|
||||
if (isStringOfObject(args[0])) {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else if (args[1]) {
|
||||
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - offset':
|
||||
case 'jQuery - height':
|
||||
case 'jQuery - innerHeight':
|
||||
case 'jQuery - width':
|
||||
case 'jQuery - innerWidth':
|
||||
case 'jQuery - scrollLeft':
|
||||
case 'jQuery - scrollTop':
|
||||
case 'jQuery - position':
|
||||
if (args[0]) {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - outerHeight':
|
||||
case 'jQuery - outerWidth':
|
||||
if (args[0] && args[0] !== 'true') {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else if (args[0] === 'true') {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
|
||||
} else {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - toggle':
|
||||
if (args[0] === 'true') {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
|
||||
} else if (args[0] === 'false') {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
|
||||
} else {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - on':
|
||||
case 'jQuery - one':
|
||||
if (isStringOfObject(args[0])) {
|
||||
return '<b>' + args[0].replace(/"\(function\)"/g, '(function)') + '</b>';
|
||||
} else if (args[1] && isPureString(args[1])) {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - off':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - live':
|
||||
case 'jQuery - bind':
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - die':
|
||||
case 'jQuery - unbind':
|
||||
if (args[0]) {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - delegate':
|
||||
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - undelegate':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'unbind namespace <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - blur':
|
||||
case 'jQuery - change':
|
||||
case 'jQuery - click':
|
||||
case 'jQuery - dblclick':
|
||||
case 'jQuery - focus':
|
||||
case 'jQuery - keydown':
|
||||
case 'jQuery - keypress':
|
||||
case 'jQuery - keyup':
|
||||
case 'jQuery - mousedown':
|
||||
case 'jQuery - mouseenter':
|
||||
case 'jQuery - mouseleave':
|
||||
case 'jQuery - mousemove':
|
||||
case 'jQuery - mouseout':
|
||||
case 'jQuery - mouseover':
|
||||
case 'jQuery - mouseup':
|
||||
case 'jQuery - resize':
|
||||
case 'jQuery - scroll':
|
||||
case 'jQuery - select':
|
||||
case 'jQuery - submit':
|
||||
if (args[0]) {
|
||||
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - error':
|
||||
case 'jQuery - focusin':
|
||||
case 'jQuery - focusout':
|
||||
case 'jQuery - hover':
|
||||
case 'jQuery - load':
|
||||
case 'jQuery - unload':
|
||||
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - removeAttr':
|
||||
case 'jQuery - removeProp':
|
||||
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - val':
|
||||
if (args[0]) {
|
||||
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - hasClass':
|
||||
case 'jQuery - addClass':
|
||||
case 'jQuery - removeClass':
|
||||
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - toggleClass':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
} else {
|
||||
return 'magic no-argument toggleClass';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - children':
|
||||
if (args[0]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - closest':
|
||||
if (args[1]) {
|
||||
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - next':
|
||||
case 'jQuery - nextAll':
|
||||
if (args[0]) {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - nextUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - offsetParent':
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - prev':
|
||||
case 'jQuery - prevAll':
|
||||
if (args[0]) {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - prevUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - parent':
|
||||
case 'jQuery - parents':
|
||||
if (args[0]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - parentsUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - siblings':
|
||||
if (args[0]) {
|
||||
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHTML(html) {
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/'
|
||||
};
|
||||
|
||||
return String(html).replace(/[&<>"'\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
function joinArgs(args) {
|
||||
var html = '<b>' + args[0] + '</b>';
|
||||
if (args[1]) {
|
||||
html += ', <b>' + args[1] + '</b>';
|
||||
if (args[2]) {
|
||||
html += ', <b>' + args[2] + '</b>';
|
||||
if (args[3]) {
|
||||
html += ', and more...';
|
||||
}
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function isStringOfObject(str) {
|
||||
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
|
||||
}
|
||||
|
||||
function isPureString(str) {
|
||||
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
|
||||
}
|
||||
|
||||
function getTimelineParamsHTML(node, onASingleLine) {
|
||||
if (isJQuery(node)) {
|
||||
return getJQueryHTML(node, onASingleLine);
|
||||
} else {
|
||||
return getNonJQueryHTML(node, onASingleLine);
|
||||
}
|
||||
}
|
||||
|
||||
function getBacktraceHTML(backtrace) {
|
||||
var html = '';
|
||||
var parsedBacktrace = parseBacktrace(backtrace);
|
||||
|
||||
if (!parsedBacktrace || parsedBacktrace.length === 0) {
|
||||
html += '<div><div>can\'t find any backtrace :/</div></div>';
|
||||
} else {
|
||||
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
|
||||
html += '<div>';
|
||||
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
|
||||
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
|
||||
if (parsedBacktrace[i].column) {
|
||||
html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
|
||||
} else {
|
||||
html += '<div>line ' + parsedBacktrace[i].line + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function parseBacktrace(str) {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var splited = str.split(' / ');
|
||||
|
||||
try {
|
||||
|
||||
splited.forEach(function(trace) {
|
||||
var fnName = null, fileAndLine;
|
||||
|
||||
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 format
|
||||
withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Yet another PhantomJS 2 format?
|
||||
withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 ERROR format
|
||||
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
fileAndLine = trace;
|
||||
} else {
|
||||
fnName = withFnResult[1];
|
||||
fileAndLine = withFnResult[2];
|
||||
}
|
||||
|
||||
// And now the second part
|
||||
var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
|
||||
|
||||
if (fileAndLineSplit === null) {
|
||||
fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||
}
|
||||
|
||||
var filePath = fileAndLineSplit[1];
|
||||
var line = fileAndLineSplit[2];
|
||||
var column = fileAndLineSplit[3];
|
||||
|
||||
// Filter phantomas code
|
||||
if (filePath.indexOf('phantomjs://') === -1) {
|
||||
out.push({
|
||||
fnName: fnName,
|
||||
filePath: filePath,
|
||||
line: line,
|
||||
column: column
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getTimelineDetailsHTML(node) {
|
||||
var html = '';
|
||||
|
||||
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
|
||||
if (node.warning || node.error) {
|
||||
html += '<div class="icon-warning"></div>';
|
||||
} else {
|
||||
html += '<div class="icon-question"></div>';
|
||||
}
|
||||
|
||||
html += '<div class="detailsOverlay">';
|
||||
html += '<div class="closeBtn">✖</div>';
|
||||
|
||||
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
|
||||
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
|
||||
} else if (node.eventNotDelegated) {
|
||||
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
|
||||
}
|
||||
|
||||
if (node.data.resultsNumber === 0) {
|
||||
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
|
||||
} else if (node.data.resultsNumber > 0) {
|
||||
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
|
||||
}
|
||||
|
||||
if (node.data.backtrace) {
|
||||
html += '<h4>Backtrace</h4>';
|
||||
html += '<div class="table">';
|
||||
html += getBacktraceHTML(node.data.backtrace);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
|
||||
|
||||
var numberWithCommas = $filter('number');
|
||||
|
||||
function getProfilerLineHTML(index, node) {
|
||||
return '<div class="index">' + (index + 1) + '</div>' +
|
||||
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
|
||||
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
|
||||
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
|
||||
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
|
||||
}
|
||||
|
||||
function recursiveChildrenHTML(node) {
|
||||
var html = '';
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function onDetailsClick(row) {
|
||||
// Close if it's alreay open
|
||||
if (row.classList.contains('showDetails')) {
|
||||
closeDetails(row);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any other open details overlay
|
||||
var openOnes = document.getElementsByClassName('showDetails');
|
||||
if (openOnes.length > 0) {
|
||||
openOnes[0].classList.remove('showDetails');
|
||||
}
|
||||
|
||||
// Make it appear
|
||||
row.classList.add('showDetails');
|
||||
|
||||
// Bind the close button
|
||||
row.querySelector('.closeBtn').addEventListener('click', function() {
|
||||
closeDetails(row);
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetails(row) {
|
||||
row.classList.remove('showDetails');
|
||||
|
||||
// Unbind the close button
|
||||
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
index: '=',
|
||||
node: '='
|
||||
},
|
||||
template: '<div></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
|
||||
if (scope.node.error) {
|
||||
element.addClass('jsError');
|
||||
} else if (scope.node.windowPerformance) {
|
||||
element.addClass('windowPerformance');
|
||||
}
|
||||
|
||||
element.append(getProfilerLineHTML(scope.index, scope.node));
|
||||
element[0].id = 'line_' + scope.index;
|
||||
|
||||
if (scope.node.warning) {
|
||||
element[0].classList.add('warning');
|
||||
|
||||
if (scope.node.queryWithoutResults) {
|
||||
element[0].classList.add('queryWithoutResults');
|
||||
}
|
||||
|
||||
if (scope.node.jQueryCallOnEmptyObject) {
|
||||
element[0].classList.add('jQueryCallOnEmptyObject');
|
||||
}
|
||||
|
||||
if (scope.node.eventNotDelegated) {
|
||||
element[0].classList.add('eventNotDelegated');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bind click on the details icon
|
||||
var detailsIcon = element[0].querySelector('.details div');
|
||||
if (detailsIcon) {
|
||||
detailsIcon.addEventListener('click', function() {
|
||||
onDetailsClick(this.parentNode.parentNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
function shortenUrl(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.filter('shortenUrl', function() {
|
||||
return shortenUrl;
|
||||
});
|
||||
|
||||
function getUrlLink(url, maxLength) {
|
||||
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
|
||||
}
|
||||
|
||||
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><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></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.filter('bytes', function() {
|
||||
return function(bytes) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
var kilo = bytes / 1024;
|
||||
|
||||
if (kilo < 1) {
|
||||
return bytes + ' bytes';
|
||||
}
|
||||
|
||||
if (kilo < 100) {
|
||||
return kilo.toFixed(1) + ' KB';
|
||||
}
|
||||
|
||||
if (kilo < 1024) {
|
||||
return kilo.toFixed(0) + ' KB';
|
||||
}
|
||||
|
||||
var mega = kilo / 1024;
|
||||
|
||||
if (mega < 10) {
|
||||
return mega.toFixed(2) + ' MB';
|
||||
}
|
||||
|
||||
return mega.toFixed(1) + ' MB';
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
|
||||
|
||||
resultsFactory.factory('Results', ['$resource', function($resource) {
|
||||
return $resource('api/results/:runId', {
|
||||
|
||||
});
|
||||
}]);
|
||||
@@ -0,0 +1,7 @@
|
||||
var runsFactory = angular.module('runsFactory', ['ngResource']);
|
||||
|
||||
runsFactory.factory('Runs', ['$resource', function($resource) {
|
||||
return $resource('api/runs/:runId', {
|
||||
|
||||
});
|
||||
}]);
|
||||
@@ -0,0 +1,61 @@
|
||||
var apiService = angular.module('apiService', []);
|
||||
|
||||
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
|
||||
|
||||
return {
|
||||
|
||||
launchTest: function(url, settings) {
|
||||
var runObject = {
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
device: settings.device,
|
||||
waitForSelector: settings.waitForSelector,
|
||||
cookie: settings.cookie,
|
||||
authUser: settings.authUser,
|
||||
authPass: settings.authPass,
|
||||
blockDomain: settings.blockDomain,
|
||||
allowedDomains: settings.allowedDomains,
|
||||
noExternals: settings.noExternals
|
||||
};
|
||||
|
||||
|
||||
if (settings.domainsBlackOrWhite === 'black') {
|
||||
runObject.blockDomain = this.parseDomains(settings.domains);
|
||||
} else if (settings.domainsBlackOrWhite === 'white') {
|
||||
var allowedDomains = this.parseDomains(settings.domains);
|
||||
if (allowedDomains.length > 0) {
|
||||
runObject.allowDomain = allowedDomains;
|
||||
} else {
|
||||
runObject.noExternals = true;
|
||||
}
|
||||
}
|
||||
|
||||
Runs.save(runObject, 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...');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
relaunchTest: function(result) {
|
||||
this.launchTest(result.params.url, result.params.options);
|
||||
},
|
||||
|
||||
parseDomains: function(textareaContent) {
|
||||
var lines = textareaContent.split('\n');
|
||||
|
||||
function removeEmptyLines (line) {
|
||||
return line.trim() !== '';
|
||||
}
|
||||
|
||||
// Remove empty lines
|
||||
return lines.filter(removeEmptyLines).join(',');
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,34 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,24 @@
|
||||
var settingsService = angular.module('settingsService', []);
|
||||
|
||||
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
|
||||
|
||||
return {
|
||||
|
||||
getMergedSettings: function() {
|
||||
var defaultSettings = {
|
||||
device: 'desktop',
|
||||
showAdvanced: false
|
||||
};
|
||||
|
||||
var savedValues = localStorageService.get('settings');
|
||||
|
||||
return angular.extend(defaultSettings, savedValues);
|
||||
},
|
||||
|
||||
saveSettings: function(settings) {
|
||||
localStorageService.set('settings', settings);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,12 @@
|
||||
.about {
|
||||
margin: 3em auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.about p {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.about a {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.phone:hover:after {
|
||||
top: 1.7em;
|
||||
left: 0.64em;
|
||||
}
|
||||
|
||||
.screenshotWrapper.tablet:hover:after {
|
||||
top: 1.5em;
|
||||
left: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
> a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
> a: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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
.promess {
|
||||
padding: 0em 2em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.price {
|
||||
padding: 0em 2em 3em;
|
||||
margin-top: 0em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.launchBtn {
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
&:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
&.disabled {
|
||||
background: #deaca6;
|
||||
&:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
input, select {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.device {
|
||||
margin-top: 3em;
|
||||
.item {
|
||||
display: inline-block;
|
||||
margin: 1em 0.75em;
|
||||
width: 5.5em;
|
||||
height: 5.5em;
|
||||
color: #FFF;
|
||||
border: 1px solid #FFF;
|
||||
padding: 1px;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
|
||||
&.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
span {
|
||||
font-size: 0.8em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 0.5em;
|
||||
width: 25em;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
font-size: 0.8em;
|
||||
border-radius: 1em;
|
||||
border: 2px solid #ffa319;
|
||||
white-space: normal;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.showAdvanced {
|
||||
display: inline-block;
|
||||
margin-top: 2em;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
}
|
||||
|
||||
.currentSettings {
|
||||
font-size: 0.9em;
|
||||
|
||||
span {
|
||||
color: #ffa319;
|
||||
&:after {
|
||||
color: #FFF;
|
||||
content: ",";
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.advanced {
|
||||
margin: 1em 0 0;
|
||||
display: table;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-spacing: 0.75em;
|
||||
|
||||
> div {
|
||||
display: table-row;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
width: 75%;
|
||||
|
||||
&.label {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subTable {
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
> div {
|
||||
display: table-row;
|
||||
> div {
|
||||
display: table-cell;
|
||||
padding: 0 0 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 6em 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;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
@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 {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #000;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screenshotError {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.desktop {
|
||||
border: 0.2em solid #AAA;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.phone {
|
||||
border: 0.07em solid #CCC;
|
||||
padding: 1em 0.3em 1.5em;
|
||||
border-radius: 0.6em;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
bottom: 0.3em;
|
||||
left: 3.3em;
|
||||
border: 0.07em solid #CCC;
|
||||
border-radius: 0.5em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 0.1em;
|
||||
bottom: 13.9em;
|
||||
left: 3.2em;
|
||||
background: #555;
|
||||
border-radius: 0.05em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 6.75em;
|
||||
height: 12em;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.tablet {
|
||||
border: 0.07em solid #CCC;
|
||||
padding: 0.8em 0.5em 0.9em;
|
||||
border-radius: 0.6em;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
bottom: 0.15em;
|
||||
left: 4.35em;
|
||||
border: 0.07em solid #CCC;
|
||||
border-radius: 0.4em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 8.25em;
|
||||
height: 11em;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #413;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.version {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.github {
|
||||
margin: 1em 0 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.statusSubMessage {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
.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 h3 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.rule .okThreshold {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.rule .message {
|
||||
width: 80%;
|
||||
margin: 1.5em 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;
|
||||
}
|
||||
}
|
||||
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.offendersTable, .value {
|
||||
.offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.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;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
> div {
|
||||
margin: 0.5em;
|
||||
|
||||
div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backtrace, .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
&.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checker {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
|
||||
> 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;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.totalWeightPie {
|
||||
max-width: 39em;
|
||||
margin: 2em auto 4em;
|
||||
|
||||
canvas {
|
||||
max-width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.hugeFile {
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.imageOffenders {
|
||||
display: table;
|
||||
border-spacing: 3em;
|
||||
width: 90%;
|
||||
|
||||
> div {
|
||||
display: table-row;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 10em;
|
||||
max-width: 40em;
|
||||
border: 1px solid #000;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.smallPreview {
|
||||
display: block;
|
||||
max-height: 4em;
|
||||
max-width: 8em;
|
||||
border: 1px solid #000;
|
||||
margin: 1em auto 0.2em;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* Timeline colors, related to Window Performances */
|
||||
@domCreationColor: #FF6600;
|
||||
@domCreationBg: #FFE0CC;
|
||||
@@ -11,145 +10,26 @@
|
||||
@domInteractiveColor: #FFE433;
|
||||
@domInteractiveBg: #FFFCCC;
|
||||
|
||||
|
||||
.resultsMenu {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.resultsMenu .menuItem {
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
width: 8em;
|
||||
height: 7em;
|
||||
color: #fff;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.resultsMenu .back {
|
||||
color: #5e2846;
|
||||
border-color: #5e2846;
|
||||
}
|
||||
.resultsMenu .menuItem div {
|
||||
padding-top: 0.5em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
|
||||
color: #ffa319;
|
||||
border-color: #ffa319;
|
||||
}
|
||||
.resultsMenu .menuItem:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
.resultsMenu span {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.summary, .metrics, .execution {
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.notations {
|
||||
display: table;
|
||||
width: 90%;
|
||||
margin: 0 10%;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
.notations > div {
|
||||
display: table-row;
|
||||
}
|
||||
.notations > div > div {
|
||||
display: table-cell;
|
||||
height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notations .notation {
|
||||
font-weight: bold;
|
||||
.execution {
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria {
|
||||
font-weight: normal;
|
||||
}
|
||||
.notations .A, .notations .B, .notations .C, .notations .D, .notations .E, .notations .F, .notations .NA {
|
||||
width: 2.5em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.notations .A {
|
||||
/* green */
|
||||
background: #00DB61;
|
||||
}
|
||||
.notations .B {
|
||||
/* green */
|
||||
background: #CAD63D;
|
||||
}
|
||||
.notations .C {
|
||||
/* yellow */
|
||||
background: #FFD119;
|
||||
}
|
||||
.notations .D {
|
||||
/* orange */
|
||||
background: #FFA319;
|
||||
}
|
||||
.notations .E {
|
||||
/* red */
|
||||
background: #FF6600;
|
||||
}
|
||||
.notations .F {
|
||||
/* red */
|
||||
background: #FF1919;
|
||||
}
|
||||
.notations .NA {
|
||||
/* Non applicable */
|
||||
background: #CCC;
|
||||
}
|
||||
.notations .icon-eye {
|
||||
color: #9c4274;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notations .criteria .table {
|
||||
width: 75%;
|
||||
}
|
||||
.notations .criteria .label {
|
||||
width: 70%;
|
||||
}
|
||||
.notations .criteria .result {
|
||||
width: 20%;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .warning .label, .notations .warning .result {
|
||||
color: #FF1919;
|
||||
}
|
||||
.notations .criteria .info {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
.notations .criteria .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.selectScript {
|
||||
padding-bottom: 2em;
|
||||
font-size: 0.9em;
|
||||
|
||||
select {
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
font-size: 0.8em;
|
||||
|
||||
select {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeline {
|
||||
margin: 2em 0 5em;
|
||||
@@ -186,6 +66,10 @@ h4 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
@@ -269,54 +153,18 @@ h4 {
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
|
||||
.metrics h4 {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.metrics .module {
|
||||
padding-left: 4em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.metrics .legend {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.metrics .offenders {
|
||||
padding-left: 0em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.metrics .offenders div {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metrics .offenders ul {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 1em 0;
|
||||
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;
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -325,7 +173,8 @@ input.textFilter {
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
|
||||
.table > div {
|
||||
.table > div,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
@@ -334,7 +183,8 @@ input.textFilter {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.table > div > div {
|
||||
.table > div > div,
|
||||
.table > a > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
@@ -351,14 +201,82 @@ input.textFilter {
|
||||
background: #f1c40f;
|
||||
}
|
||||
|
||||
.table > div.highlight {
|
||||
> div.startTime {
|
||||
background-color: #C0F090;
|
||||
}
|
||||
}
|
||||
.table > div.highlight-remove {
|
||||
transition: 3s;
|
||||
|
||||
> div.startTime {
|
||||
transition: background-color 3s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.table > div > .type {
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.table .children {
|
||||
margin-top: 0.2em;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.table .child {
|
||||
margin-left: 0.5em;
|
||||
|
||||
> .child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "↳";
|
||||
}
|
||||
|
||||
.childArgs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
background: #EBD8E2;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.childArgs {
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: 0 1em 0 2em;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background: #EBD8E2;
|
||||
line-height: 1.3em;
|
||||
height: 1.3em;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table .showingDetails .child span:hover {
|
||||
background: inherit;
|
||||
|
||||
.childArgs {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
@@ -371,11 +289,13 @@ input.textFilter {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table .details .icon-warning {
|
||||
cursor: pointer;
|
||||
.table .icon-warning {
|
||||
display: inline-block;
|
||||
width: 0.8em;
|
||||
}
|
||||
|
||||
.detailsOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
@@ -385,13 +305,16 @@ input.textFilter {
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.showDetails .detailsOverlay {
|
||||
display: block;
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
@@ -426,72 +349,39 @@ input.textFilter {
|
||||
.table > div > .startTime.domCreation {
|
||||
background: @domCreationBg;
|
||||
}
|
||||
|
||||
.table .icon-warning {
|
||||
.execution .icon-warning {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
|
||||
/**** NgModal popin (have a look inside bower_components) ****/
|
||||
.ng-modal {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.ng-modal-overlay {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ng-modal-dialog {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border: 3px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
.ng-modal-dialog-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-wrap: break-word;
|
||||
max-height: 20em;
|
||||
font-weight: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
.ng-modal-close {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.ng-modal-close-x {
|
||||
font-weight: bold;
|
||||
font-family: Arial, sans-serif;
|
||||
.queryWithoutResultsFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.queryWithoutResults {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ng-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: solid 1px #999;
|
||||
.jQueryCallOnEmptyObjectFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.jQueryCallOnEmptyObject {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.eventNotDelegatedFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.eventNotDelegated {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.jsErrorFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.jsError {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools</title>
|
||||
<base href="<%= baseUrl %>">
|
||||
<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">
|
||||
<link rel="stylesheet" type="text/css" href="node_modules/angular-chart.js/dist/angular-chart.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
<link rel="preconnect" href="//www.google-analytics.com">
|
||||
<link rel="dns-prefetch" href="//www.google-analytics.com">
|
||||
<link rel="preconnect" href="//ghbtns.com">
|
||||
<link rel="dns-prefetch" href="//ghbtns.com">
|
||||
<link rel="preconnect" href="https://api.github.com">
|
||||
<link rel="dns-prefetch" href="https://api.github.com">
|
||||
|
||||
<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">
|
||||
<span class="version">@@version</span>
|
||||
<br><a href="<%= baseUrl %>about">More about Yellow Lab Tools</a><br>
|
||||
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
|
||||
</div>
|
||||
|
||||
<!-- build:js js/all.js -->
|
||||
<script src="node_modules/angular/angular.min.js"></script>
|
||||
<script src="node_modules/chart.js/Chart.min.js"></script>
|
||||
<script src="node_modules/angular-route/angular-route.min.js"></script>
|
||||
<script src="node_modules/angular-resource/angular-resource.min.js"></script>
|
||||
<script src="node_modules/angular-sanitize/angular-sanitize.min.js"></script>
|
||||
<script src="node_modules/angular-animate/angular-animate.min.js"></script>
|
||||
<script src="node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
|
||||
<script src="node_modules/angular-chart.js/dist/angular-chart.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/services/settingsService.js"></script>
|
||||
|
||||
<script src="js/directives/gradeDirective.js"></script>
|
||||
<script src="js/directives/offendersDirectives.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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="<%= baseUrl %>">Back to index</a></p>
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="summary board">
|
||||
|
||||
<div ng-if="result.blockedRequests">
|
||||
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
|
||||
<div ng-repeat="request in result.blockedRequests">
|
||||
{{request}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
|
||||
</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">
|
||||
<a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
|
||||
ng-class="{'warning': rule.abnormal}" href="/result/{{runId}}/rule/{{ruleName}}">
|
||||
<div class="grade">
|
||||
<grade score="rule.score"></grade>
|
||||
</div>
|
||||
<div class="label">{{rule.policy.label}}</div>
|
||||
<div class="result">
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
||||
<b>Did you know?</b> You can <b>bookmark</b> this result page URL or <b>share it</b>, Yellow Lab Tools keeps test results for years!
|
||||
</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="<%= baseUrl %>">Test another webpage</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="error">
|
||||
<h2>Run failed / Run not found</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
|
||||
<p class="price">Free and open source!</p>
|
||||
|
||||
<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}" />
|
||||
<div class="settings">
|
||||
<div class="device">
|
||||
<div>Choose the simulated device:</div>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
|
||||
</div>
|
||||
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
|
||||
<span ng-if="!settings.showAdvanced">Advanced settings ✚</span>
|
||||
<span ng-if="settings.showAdvanced">Hide advanced settings ✖</span>
|
||||
</a> ]
|
||||
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
|
||||
Currently set:
|
||||
<span ng-if="settings.waitForSelector">wait for selector</span>
|
||||
<span ng-if="settings.cookie">cookie</span>
|
||||
<span ng-if="settings.authUser || settings.authPass">authentication</span>
|
||||
</span>
|
||||
<div class="advanced" ng-show="settings.showAdvanced">
|
||||
<!--<div>
|
||||
<div class="label">
|
||||
Wait selector
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
|
||||
</span>
|
||||
</div>
|
||||
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
|
||||
</div>-->
|
||||
<div>
|
||||
<div class="label">
|
||||
Cookie
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
|
||||
</span>
|
||||
</div>
|
||||
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Authent
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="subTable">
|
||||
<div>
|
||||
<div>username</div>
|
||||
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div><span>password</div>
|
||||
<div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Block domains
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
|
||||
</div>
|
||||
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -0,0 +1,36 @@
|
||||
<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="<%= baseUrl %>">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 just rebooted. We are very sorry about that, please try to launch the test again.</p>
|
||||
|
||||
<a class="linkButton" href="<%= baseUrl %>">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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<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="<%= baseUrl %>"><div class="icon-arrow-left3"></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-list"></div><span>Dashboard</span></div>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
|
||||
</div>
|
||||
@@ -0,0 +1,424 @@
|
||||
<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:
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
</h3>
|
||||
<div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
|
||||
Have
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
|
||||
<span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
|
||||
<span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
|
||||
to get the 100/100 score.
|
||||
</div>
|
||||
<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-if="rule.offendersObj.count >= 0"><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 === '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 === 'eventsScrollBound'">
|
||||
<span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
|
||||
<span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
|
||||
<span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
|
||||
<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 === '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 === 'jQueryFunctionsUsed'">
|
||||
function <b>{{offender.functionName}}</b> used {{offender.count}} times
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'documentWriteCalls'">
|
||||
<b>{{offender.writeFn}}</b>
|
||||
<span ng-if="offender.from">
|
||||
called from
|
||||
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
|
||||
<url-link url="offender.from.file" max-length="50"></url-link>
|
||||
line {{offender.from.line}}
|
||||
</span>
|
||||
<span ng-if="!offender.from">
|
||||
called from (no backtrace available)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssRules'">
|
||||
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
|
||||
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
|
||||
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'similarColors'">
|
||||
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</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 === 'cssImports'">
|
||||
{{offender.css}}
|
||||
<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 === 'cssOldPropertyPrefixes'">
|
||||
<b>{{offender.property}} {{offender.message}}</b>
|
||||
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
|
||||
<span ng-if="!offender.showMore">show</span>
|
||||
<span ng-if="offender.showMore">hide</span>
|
||||
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
|
||||
</div>
|
||||
<div ng-if="offender.showMore" class="smallerOffenders">
|
||||
<div ng-repeat="cssRule in offender.rules">
|
||||
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
|
||||
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
|
||||
<img ng-src="{{offender}}" class="smallPreview checker"></img>
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || 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' || policyName === 'synchronousXHR'">
|
||||
{{offender}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="(file, fileDetails) in rule.offendersObj.byFile track by $index">
|
||||
<h3>
|
||||
<ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
|
||||
in
|
||||
<url-link ng-if="file !== 'Inline CSS'" url="file" max-length="80"></url-link>
|
||||
<span ng-if="file === 'Inline CSS'">inline CSS</span>
|
||||
</h3>
|
||||
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in fileDetails.offenders track by $index">
|
||||
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || 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>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssMobileFirst'">
|
||||
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssDuplicatedProperties'">
|
||||
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssEmptyRules'">
|
||||
<b>{{offender.css}} { }</b>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssExpressions'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssImportants'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldIEFixes'">
|
||||
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
|
||||
<span ng-bind-html="offender.bolded"></span>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" class="offendersHtml">
|
||||
|
||||
<div ng-if="policyName === 'DOMelementMaxDepth'">
|
||||
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
|
||||
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
|
||||
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
</div>
|
||||
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
||||
<p>This is the colors palette, sized by total occurrences:</p>
|
||||
<div class="colorPalette checker">
|
||||
<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="policyName === 'totalWeight'">
|
||||
<h3>Weight by MIME type</h3>
|
||||
<div class="totalWeightPie">
|
||||
<canvas class="chart chart-doughnut" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></canvas>
|
||||
</div>
|
||||
<div ng-repeat="type in weightLabels">
|
||||
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
|
||||
<div><url-link url="request.url" max-length="60"></url-link></div>
|
||||
<div ng-class="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'imageOptimization'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
|
||||
<div class="imageOffenders">
|
||||
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
|
||||
<div>
|
||||
Current file: <url-link url="image.url" max-length="50"></url-link>
|
||||
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url}}" class="checker" /></a></div>
|
||||
</div>
|
||||
<div>
|
||||
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
|
||||
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
|
||||
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
|
||||
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
|
||||
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
|
||||
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'gzipCompression'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div>File</div>
|
||||
<div>Current weight</div>
|
||||
<div>Gzipped</div>
|
||||
<div>Gain</div>
|
||||
</div>
|
||||
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
|
||||
<div>
|
||||
<url-link url="file.url" max-length="70"></url-link>
|
||||
</div>
|
||||
<div>{{file.original | bytes}}</div>
|
||||
<div>{{file.gzipped | bytes}}</div>
|
||||
<div><b>-{{file.gain | bytes}}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'fileMinification'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div>File</div>
|
||||
<div>Current weight</div>
|
||||
<div>Minified</div>
|
||||
<div>Gain</div>
|
||||
</div>
|
||||
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
|
||||
<div>
|
||||
<url-link url="file.url" max-length="70"></url-link>
|
||||
</div>
|
||||
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
|
||||
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
|
||||
<div><b>-{{file.gain | bytes}}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'totalRequests'">
|
||||
<h3>Requests by MIME type</h3>
|
||||
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
|
||||
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
|
||||
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
|
||||
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
|
||||
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
|
||||
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
|
||||
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="request in requests track by $index">
|
||||
<div><url-link url="request" max-length="100"></url-link></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'identicalFiles'">
|
||||
<div ng-repeat="offender in rule.offendersObj.list track by $index">
|
||||
<h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="url in offender.urls">
|
||||
<div><url-link url="url" max-length="100"></url-link></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'smallRequests'">
|
||||
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
|
||||
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
|
||||
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
|
||||
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
|
||||
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div ng-if="type == 'image'">Preview</div>
|
||||
<div>File</div>
|
||||
<div>Weight (bytes)</div>
|
||||
</div>
|
||||
<div ng-repeat="request in requests track by $index">
|
||||
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
|
||||
<div><url-link url="request.url" max-length="100"></url-link></div>
|
||||
<div>{{request.size}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMaccesses'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'queriesWithoutResults'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssBreakpoints'">
|
||||
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
|
||||
<h3>Breakpoints distribution graph</h3>
|
||||
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
|
||||
<h3>Breakpoints list</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
|
||||
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="rule.value === 0">
|
||||
No breakpoint
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'http2'">
|
||||
<h3>Protocols advertised by the server</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="protocol in rule.offendersObj.list">
|
||||
<div>{{protocol}}</div>
|
||||
</div>
|
||||
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
|
||||
<div>none</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>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="screenshot board">
|
||||
<h2>Screenshot</h2>
|
||||
|
||||
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
|
||||
</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>
|
||||
@@ -0,0 +1,100 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="execution board">
|
||||
<div class="selectScript" ng-class="{empty:!selectedScript}">
|
||||
Filter timeline and profiler by script:
|
||||
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
|
||||
<option value="">All (no filter)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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.javascriptExecutionTree.data.domInteractive,
|
||||
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
|
||||
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
|
||||
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
|
||||
}">
|
||||
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></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 the JavaScript and the DOM. It is useful to understand what happens while the page loads.
|
||||
</p>
|
||||
<div class="filters">
|
||||
<div>
|
||||
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Filter on warnings and errors</label>
|
||||
<div class="subFilters" ng-if="warningsFilterOn">
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
|
||||
<label for="queryWithoutResultsFilterOn">Queries without results</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
|
||||
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
|
||||
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
|
||||
<label for="jsErrorFilterOn">Errors</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{queryWithoutResultsFilterOn}}
|
||||
</div>
|
||||
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
<div>Timestamp</div>
|
||||
</div>
|
||||
|
||||
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
var Q = require('q');
|
||||
|
||||
var Runner = require('./runner');
|
||||
|
||||
|
||||
var yellowLabTools = function(url, options) {
|
||||
'use strict';
|
||||
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!url) {
|
||||
|
||||
deferred.reject('URL missing');
|
||||
|
||||
} else {
|
||||
|
||||
if (url.toLowerCase().indexOf('http://') !== 0 && url.toLowerCase().indexOf('https://') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
var params = {
|
||||
url: url,
|
||||
options: options || {}
|
||||
};
|
||||
|
||||
var runner = new Runner(params)
|
||||
.then(function(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.fail(function(err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
module.exports = yellowLabTools;
|
||||