Compare commits
894
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b23f6be47d | ||
|
|
276e6503f7 | ||
|
|
4f23139df0 | ||
|
|
f83375b002 | ||
|
|
77b7912aed | ||
|
|
291607522b | ||
|
|
4e78b994b5 | ||
|
|
288e034ec0 | ||
|
|
480eaf1cf5 | ||
|
|
d3adfe50e1 | ||
|
|
14d51f9123 | ||
|
|
b19b7bbbf9 | ||
|
|
7d15094f9c | ||
|
|
914a89b66c | ||
|
|
26466f5a6f | ||
|
|
7fc2fd56f0 | ||
|
|
1977cf6637 | ||
|
|
4626c18ced | ||
|
|
5b2853cca2 | ||
|
|
7d63f309c4 | ||
|
|
aa4f904767 | ||
|
|
4067eea89f | ||
|
|
d780ac32ee | ||
|
|
742dee9202 | ||
|
|
a1e2c11d8a | ||
|
|
471d2c0b50 | ||
|
|
be2b32ffd1 | ||
|
|
db5e4766af | ||
|
|
f9f151e89e | ||
|
|
7e88a3897c | ||
|
|
63e43db0c2 | ||
|
|
43c1f02b6f | ||
|
|
6bdaee3a00 | ||
|
|
7f74543125 | ||
|
|
514eeb8466 | ||
|
|
403f343371 | ||
|
|
8c5547da64 | ||
|
|
eaac387c8d | ||
|
|
00aad4da47 | ||
|
|
c10c844fa9 | ||
|
|
40c1ae1d38 | ||
|
|
3ae6888fee | ||
|
|
14b5e4e184 | ||
|
|
530e076838 | ||
|
|
1be51d6679 | ||
|
|
b0c2b04a2d | ||
|
|
4e2990e599 | ||
|
|
9cd49c2447 | ||
|
|
0cdecee771 | ||
|
|
1bcf4e6e8d | ||
|
|
343f7e4fd5 | ||
|
|
4634b51edf | ||
|
|
8cd0442a1f | ||
|
|
10a536b1a2 | ||
|
|
65b7916a6a | ||
|
|
2cf96a2ce3 | ||
|
|
7885cb80f4 | ||
|
|
1356382524 | ||
|
|
04407fe8ff | ||
|
|
3451100b80 | ||
|
|
f8927b1843 | ||
|
|
c7bf8bb1ba | ||
|
|
c6010a6734 | ||
|
|
cf46213e00 | ||
|
|
2ee786f351 | ||
|
|
92d5690bba | ||
|
|
791a79a234 | ||
|
|
fa798f3ecd | ||
|
|
f72739d98d | ||
|
|
fd581b4453 | ||
|
|
747a63d452 | ||
|
|
833a2bf5d6 | ||
|
|
607b7ecd1f | ||
|
|
bb5d36402a | ||
|
|
ccd1f5f8e9 | ||
|
|
2c5f41b580 | ||
|
|
2f5b494885 | ||
|
|
58f87b39aa | ||
|
|
d478ecfd41 | ||
|
|
72fc0cce07 | ||
|
|
81f63bebe6 | ||
|
|
102087537a | ||
|
|
6dac45172e | ||
|
|
84952b9056 | ||
|
|
e851fba71b | ||
|
|
3a05a4851f | ||
|
|
edc5710e32 | ||
|
|
1802792795 | ||
|
|
28e89beb43 | ||
|
|
889756ebb0 | ||
|
|
54b5e248e3 | ||
|
|
d24383b6ea | ||
|
|
2dc22004a1 | ||
|
|
0906febe95 | ||
|
|
b12bd8ee85 | ||
|
|
e1bfb7095b | ||
|
|
068f93c258 | ||
|
|
d8401e09c9 | ||
|
|
394ac85c32 | ||
|
|
f6e3290aa4 | ||
|
|
4925e99166 | ||
|
|
3bb5075a98 | ||
|
|
59efb7d9e5 | ||
|
|
165e95c480 | ||
|
|
31d8653ba6 | ||
|
|
80df4955e2 | ||
|
|
5436277ada | ||
|
|
ed64719560 | ||
|
|
ac659066c6 | ||
|
|
3a4d17f054 | ||
|
|
57e4d269eb | ||
|
|
c2339c3522 | ||
|
|
13065ac726 | ||
|
|
af91a72187 | ||
|
|
98b835227e | ||
|
|
c633dd0919 | ||
|
|
fbf928e6e4 | ||
|
|
8f63667282 | ||
|
|
e2896b2911 | ||
|
|
77fbf0f8a3 | ||
|
|
c89f33e2f8 | ||
|
|
e5b6393257 | ||
|
|
954e844a21 | ||
|
|
13acc3af86 | ||
|
|
079112c9d2 | ||
|
|
0be8dcc000 | ||
|
|
d7d040b0f9 | ||
|
|
f652229718 | ||
|
|
f727dde74b | ||
|
|
92179fe61c | ||
|
|
ecfcf3be4d | ||
|
|
be132d867a | ||
|
|
e6951012f0 | ||
|
|
3214ec075b | ||
|
|
be1d2b4b92 | ||
|
|
a5eb2cdd9a | ||
|
|
9b077969e7 | ||
|
|
7160790596 | ||
|
|
5c3e81e6dc | ||
|
|
846172a709 | ||
|
|
f62686fdeb | ||
|
|
c6511cfb55 | ||
|
|
69b5452af5 | ||
|
|
9291236733 | ||
|
|
3b3ae7fde9 | ||
|
|
d9748d5ef1 | ||
|
|
d01909830d | ||
|
|
03ecc3d6c9 | ||
|
|
1630e11c4d | ||
|
|
99fb6ab743 | ||
|
|
35bc095760 | ||
|
|
a44ad63230 | ||
|
|
ddbaf0d530 | ||
|
|
437deaf986 | ||
|
|
54df6b2863 | ||
|
|
69b98cb323 | ||
|
|
4d172761ce | ||
|
|
8c4324ee9d | ||
|
|
2e295e6891 | ||
|
|
351c891a5a | ||
|
|
bcb1e8e4f5 | ||
|
|
4997ac99cf | ||
|
|
e3bbabd63e | ||
|
|
403c558f9b | ||
|
|
31d2c1d603 | ||
|
|
a7adeeadb1 | ||
|
|
f7d7141a59 | ||
|
|
2371d4691d | ||
|
|
9ec0437673 | ||
|
|
89525b7f7b | ||
|
|
ad227bcf79 | ||
|
|
8b82d532e0 | ||
|
|
f4af47728f | ||
|
|
b3a4a9ba04 | ||
|
|
13028c6cfe | ||
|
|
7d0f6c9deb | ||
|
|
8d2901cc58 | ||
|
|
bbc7b6d172 | ||
|
|
145d1e5fdb | ||
|
|
eae4e42dcc | ||
|
|
6886261692 | ||
|
|
23f2d85e70 | ||
|
|
d1d598940d | ||
|
|
efb98d28ef | ||
|
|
462c822255 | ||
|
|
9e30b1816f | ||
|
|
d79929d1c9 | ||
|
|
0286678286 | ||
|
|
4896d7c1be | ||
|
|
b06a2337a0 | ||
|
|
20bcdd1f90 | ||
|
|
24d57336a9 | ||
|
|
91397c963a | ||
|
|
995b773c74 | ||
|
|
c561abd9f8 | ||
|
|
10dd0fa438 | ||
|
|
d74b0bf6e1 | ||
|
|
913924d8d3 | ||
|
|
7bc9ce4391 | ||
|
|
ed5334a7d6 | ||
|
|
120f22c6fc | ||
|
|
f8a17dac00 | ||
|
|
7f41feb959 | ||
|
|
da7f584da7 | ||
|
|
031a88f4c4 | ||
|
|
e2ff12415c | ||
|
|
edd59cf462 | ||
|
|
b52f681e3c | ||
|
|
5d33d723e9 | ||
|
|
5e1a8b1ec7 | ||
|
|
3c99105b23 | ||
|
|
0b298ea426 | ||
|
|
7ed36f8736 | ||
|
|
34d2b7f24e | ||
|
|
a2303d35e8 | ||
|
|
c87d9ab74f | ||
|
|
e3a0806eee | ||
|
|
b63c4dfe21 | ||
|
|
2277d7d234 | ||
|
|
f76ce2be14 | ||
|
|
3c74208ab9 | ||
|
|
5ae3f42313 | ||
|
|
af5c4bfe76 | ||
|
|
9c25769d8e | ||
|
|
12a4bf67f6 | ||
|
|
34a858e948 | ||
|
|
3a879b0186 | ||
|
|
dbe72b32b2 | ||
|
|
917aa60c98 | ||
|
|
84e0f5e6ff | ||
|
|
7b89d76cea | ||
|
|
fbf031b06d | ||
|
|
671e855b0e | ||
|
|
8aa32ca3fa | ||
|
|
07c767c4fa | ||
|
|
4ce0782a05 | ||
|
|
7456c65799 | ||
|
|
8cf31b8afc | ||
|
|
ff70595a15 | ||
|
|
f6f19a0a07 | ||
|
|
b83b964678 | ||
|
|
f574b6b9c2 | ||
|
|
7ee29dcbc5 | ||
|
|
d348361889 | ||
|
|
07a78b4ad7 | ||
|
|
a26ebbccc1 | ||
|
|
ccb020e885 | ||
|
|
76cb23e233 | ||
|
|
4686e13390 | ||
|
|
584f762e11 | ||
|
|
8528811992 | ||
|
|
9147f64b65 | ||
|
|
f7f2e71ee1 | ||
|
|
f9e1dcca6c | ||
|
|
e11388491f | ||
|
|
eb0cff36c9 | ||
|
|
ac8141ab15 | ||
|
|
b3b13e550d | ||
|
|
ba1767e312 | ||
|
|
10048dadec | ||
|
|
815bb11cde | ||
|
|
7372f5583c | ||
|
|
9115e208ac | ||
|
|
24bb1b58a0 | ||
|
|
cfca70ae14 | ||
|
|
1db6da7024 | ||
|
|
c212c568e9 | ||
|
|
1d437fb81e | ||
|
|
5a0478ad56 | ||
|
|
039c191d1f | ||
|
|
eb4b015de8 | ||
|
|
5269bfbf8e | ||
|
|
ed44c20281 | ||
|
|
878323a299 | ||
|
|
0dc2363962 | ||
|
|
72d660f1ac | ||
|
|
46532a861f | ||
|
|
4c39908748 | ||
|
|
35081fd311 | ||
|
|
7463e4e3eb | ||
|
|
66ce786726 | ||
|
|
682a50da53 | ||
|
|
d372abd5a1 | ||
|
|
dfb5a6629f | ||
|
|
bb72f7e70a | ||
|
|
37085042f8 | ||
|
|
2cbcb320fe | ||
|
|
949556e2fa | ||
|
|
08d1d3602e | ||
|
|
910c45e457 | ||
|
|
6f031d0c7e | ||
|
|
44c9d572c8 | ||
|
|
05949d2f87 | ||
|
|
ad695e43b9 | ||
|
|
87015f7133 | ||
|
|
deb019aa4a | ||
|
|
c79d014305 | ||
|
|
182101023b | ||
|
|
f1b2f51a06 | ||
|
|
a5c0ad5998 | ||
|
|
6fd250d4d1 | ||
|
|
db2270c8d8 | ||
|
|
10f19069c6 | ||
|
|
13a7ea07a8 | ||
|
|
aacd795ae0 | ||
|
|
566e7b2f40 | ||
|
|
f413b9e070 | ||
|
|
6e4206a8e2 | ||
|
|
954aa48f52 | ||
|
|
461ac91b32 | ||
|
|
8a9cc278ec | ||
|
|
9db87944f2 | ||
|
|
9230981e54 | ||
|
|
72b13dd941 | ||
|
|
e6b0c8ff05 | ||
|
|
f0d967f0e4 | ||
|
|
85b9aab015 | ||
|
|
69571f668c | ||
|
|
a89dd87c16 | ||
|
|
22b03eee29 | ||
|
|
e3ea3fcdfa | ||
|
|
ccfbde9a92 | ||
|
|
7358860bfc | ||
|
|
17b355197c | ||
|
|
aa7e50cc6c | ||
|
|
69ce105806 | ||
|
|
cafdfcca51 | ||
|
|
a8c5c9c34e | ||
|
|
6dd83675fc | ||
|
|
b65fad6214 | ||
|
|
884046ba3f | ||
|
|
6d327adb83 | ||
|
|
eddb623fba | ||
|
|
2ea8e831cd | ||
|
|
270c981051 | ||
|
|
7971566159 | ||
|
|
9fbf2d5ef6 | ||
|
|
21bf2968a9 | ||
|
|
117950c922 | ||
|
|
b4c56f7998 | ||
|
|
af0c802486 | ||
|
|
33d518f383 | ||
|
|
1f9fe89f87 | ||
|
|
5fef4f1d49 | ||
|
|
d6446872ee | ||
|
|
5b5feb2515 | ||
|
|
b1bca1b55b | ||
|
|
4e1a70c7ac | ||
|
|
54f25e4b91 | ||
|
|
aa176f2c12 | ||
|
|
58895620c1 | ||
|
|
e10aade895 | ||
|
|
1fdd7c343b | ||
|
|
e36b08cd14 | ||
|
|
6ac9ad1cac | ||
|
|
36bf90e81b | ||
|
|
22b9acf386 | ||
|
|
e4e75acdac | ||
|
|
7996736592 | ||
|
|
3ad8f624cf | ||
|
|
0657aeb07d | ||
|
|
723971e423 | ||
|
|
86b67a1b65 | ||
|
|
d94b4daa70 | ||
|
|
ad62e85b3a | ||
|
|
133eec8163 | ||
|
|
81bac5950c | ||
|
|
651046ab68 | ||
|
|
9bd13ac29e | ||
|
|
c7d3d612ae | ||
|
|
69dd2ab20f | ||
|
|
f0b8b00461 | ||
|
|
2b8aaf1d46 | ||
|
|
ee69283a23 | ||
|
|
caaf5f7986 | ||
|
|
392964ffd2 | ||
|
|
1f76412790 | ||
|
|
555254e8fe | ||
|
|
3aa55f83b1 | ||
|
|
d15a8a88a6 | ||
|
|
1f7ab9d22d | ||
|
|
44945b2912 | ||
|
|
8d50caa86e | ||
|
|
b59eba76a6 | ||
|
|
070cde9ecb | ||
|
|
1e81e4c68f | ||
|
|
402ca7d765 | ||
|
|
d0ca7f311c | ||
|
|
e553627ee5 | ||
|
|
cbdac71025 | ||
|
|
6f35ae9857 | ||
|
|
20d64cc7ae | ||
|
|
654e18aacf | ||
|
|
75dd5c1d93 | ||
|
|
f8f9c3404a | ||
|
|
01e5817b24 | ||
|
|
96608bd005 | ||
|
|
57a5b38509 | ||
|
|
83c53188e3 | ||
|
|
6ba7f93f69 | ||
|
|
702a486cce | ||
|
|
eb0ed1ad86 | ||
|
|
e264b3a5b8 | ||
|
|
667d15c627 | ||
|
|
bd06e1d559 | ||
|
|
459aca5291 | ||
|
|
e7cd90b6ab | ||
|
|
940db70447 | ||
|
|
59139ff323 | ||
|
|
22fed1bfbc | ||
|
|
5e593bd36e | ||
|
|
ba1dfc6de4 | ||
|
|
c81f87dcdc | ||
|
|
a8b55ca3f0 | ||
|
|
0cefaf633c | ||
|
|
ba4aebd005 | ||
|
|
7b32a92290 | ||
|
|
41b99209f1 | ||
|
|
c1612fe298 | ||
|
|
e69a09d332 | ||
|
|
4e0d9fdb0b | ||
|
|
8b2e769fca | ||
|
|
d29a90a472 | ||
|
|
2e4beb0652 | ||
|
|
addb041816 | ||
|
|
4ff71b5dce | ||
|
|
a0ae62d6f5 | ||
|
|
ea18aa7551 | ||
|
|
68e7069e92 | ||
|
|
556d929b67 | ||
|
|
7f3469a0f2 | ||
|
|
a062c4aadb | ||
|
|
3d3f81ad52 | ||
|
|
fc0fb0b3d3 | ||
|
|
884586f0af | ||
|
|
f8c18afbcf | ||
|
|
8046177d84 | ||
|
|
8d8fc91391 | ||
|
|
796fda159e | ||
|
|
4b2389dafd | ||
|
|
2dd7e5937f | ||
|
|
e9371029f3 | ||
|
|
bbec60ff02 | ||
|
|
3bf4f080c5 | ||
|
|
637c043f5b | ||
|
|
9eed993421 | ||
|
|
aff720c1f1 | ||
|
|
655de30df5 | ||
|
|
fe6217bd82 | ||
|
|
eacd7b2503 | ||
|
|
2e17fb9dd3 | ||
|
|
249543d64f | ||
|
|
8ebd490260 | ||
|
|
c0dfec8b34 | ||
|
|
56a023c98a | ||
|
|
adcac881a7 | ||
|
|
f73342d902 | ||
|
|
45b0233c21 | ||
|
|
db349519cf | ||
|
|
c760a1b1fe | ||
|
|
c5fa9039b4 | ||
|
|
7eaafc52ce | ||
|
|
63239893ab | ||
|
|
e9a1a1ced0 | ||
|
|
6dc45c9830 | ||
|
|
58f7659d55 | ||
|
|
fd12d3a0b0 | ||
|
|
f25821e84d | ||
|
|
c0aa0e0509 | ||
|
|
55fbb7522b | ||
|
|
15b4d496fd | ||
|
|
ad54ad0614 | ||
|
|
b18bbd0e82 | ||
|
|
e028e45e93 | ||
|
|
0e7ac85f90 | ||
|
|
8bb2541862 | ||
|
|
133e61befd | ||
|
|
9e98680861 | ||
|
|
b75fd673cd | ||
|
|
4a7e20b3a5 | ||
|
|
bbe746c564 | ||
|
|
189faa4def | ||
|
|
c1a8d89938 | ||
|
|
71ce9affbe | ||
|
|
552dba5abe | ||
|
|
04e3246976 | ||
|
|
5d73c3d194 | ||
|
|
12d9a98831 | ||
|
|
8f646a2843 | ||
|
|
f53e9d6549 | ||
|
|
fc9c152553 | ||
|
|
00cdee831d | ||
|
|
17ae05a623 | ||
|
|
8b1a03713b | ||
|
|
e4b57033b4 | ||
|
|
62a6fb8913 | ||
|
|
0d89e22ed2 | ||
|
|
ff2aae213c | ||
|
|
43e64d8219 | ||
|
|
6823fe5d24 | ||
|
|
f683085618 | ||
|
|
71a22dc466 | ||
|
|
eb508a3ec9 | ||
|
|
aacd09d8e2 | ||
|
|
25a27dfa81 | ||
|
|
36349778e3 | ||
|
|
9271d42db5 | ||
|
|
90d6e98b51 | ||
|
|
dd6b285cd3 | ||
|
|
8b0d5d76a2 | ||
|
|
85b69bde15 | ||
|
|
dd812d0501 | ||
|
|
a79f825ff1 | ||
|
|
862e5629e3 | ||
|
|
c5a91e10df | ||
|
|
0b0e10baa8 | ||
|
|
ec18a3c443 | ||
|
|
a0867ed688 | ||
|
|
cdf8e68ff2 | ||
|
|
af92303e38 | ||
|
|
504041d75a | ||
|
|
06ab965413 | ||
|
|
42bd4884fd | ||
|
|
0c057243bc | ||
|
|
8fdc6e9638 | ||
|
|
187f7409ce | ||
|
|
e3ce6e8b4b | ||
|
|
cfe481759f | ||
|
|
6bcef33d05 | ||
|
|
aa8c9bad9f | ||
|
|
1723886f3a | ||
|
|
63c7f7b6fe | ||
|
|
f35dfaf525 | ||
|
|
eead2d20f5 | ||
|
|
f73c2540c6 | ||
|
|
05c1924940 | ||
|
|
b2f3f902b2 | ||
|
|
33fbba18ca | ||
|
|
cea3c69239 | ||
|
|
ff91c0a909 | ||
|
|
d1efda4c50 | ||
|
|
2b1d1ba2f4 | ||
|
|
9a237b5f18 | ||
|
|
fc21d1d245 | ||
|
|
0c6d4df14b | ||
|
|
8df5d8dcb4 | ||
|
|
aa9f285f59 | ||
|
|
6173022a15 | ||
|
|
afb479607b | ||
|
|
342c361184 | ||
|
|
790d5612f7 | ||
|
|
99a4e330e8 | ||
|
|
c3b702849f | ||
|
|
a49acd9220 | ||
|
|
dd3b4dc5fb | ||
|
|
155817a0f1 | ||
|
|
89c4629ec2 | ||
|
|
6ca81b3a66 | ||
|
|
b278e14ffd | ||
|
|
cb6f7cb4b6 | ||
|
|
f94feb780a | ||
|
|
0ef1b27ae5 | ||
|
|
64994b3336 | ||
|
|
6966b303ff | ||
|
|
ab8e01bbf7 | ||
|
|
e25633636b | ||
|
|
d87a209c4a | ||
|
|
a33cf1f488 | ||
|
|
91236006d4 | ||
|
|
fd9baf7a06 | ||
|
|
ed646ccba2 | ||
|
|
7bc358d7ac | ||
|
|
14fc287071 | ||
|
|
cdf7dced8b | ||
|
|
552c59218c | ||
|
|
c7e0e36902 | ||
|
|
1f82c6682b | ||
|
|
21261ee813 | ||
|
|
15deb8ea79 | ||
|
|
f7f99a162e | ||
|
|
c05f19edd2 | ||
|
|
407e88b27f | ||
|
|
512113877b | ||
|
|
8f7cb50dd4 | ||
|
|
7dedb84504 | ||
|
|
b2ca5c77f6 | ||
|
|
11d720465b | ||
|
|
c2643bfb6c | ||
|
|
3d4d4cf709 | ||
|
|
b965a5f3ae | ||
|
|
73463cd7e3 | ||
|
|
6158954d97 | ||
|
|
3190eebf6e | ||
|
|
dfe994b341 | ||
|
|
92722d0922 | ||
|
|
e04c1bdaec | ||
|
|
20b16cf174 | ||
|
|
5ffa27f545 | ||
|
|
00cdbe5a1c | ||
|
|
dffb865a66 | ||
|
|
5e581e912f | ||
|
|
256d015967 | ||
|
|
d9b3f3f6c6 | ||
|
|
09fa75f5ec | ||
|
|
38c2937f64 | ||
|
|
06d5bc734b | ||
|
|
a521541cb7 | ||
|
|
7d61252e6a | ||
|
|
cb2b9ac537 | ||
|
|
af31f8cf6f | ||
|
|
87c1245a3f | ||
|
|
49d9417cac | ||
|
|
1f29235538 | ||
|
|
fd1e36b21a | ||
|
|
7db11c7cfd | ||
|
|
33888d583b | ||
|
|
3959cdba3a | ||
|
|
200b367e4c | ||
|
|
956d2dfd07 | ||
|
|
83e50d376e | ||
|
|
802a667907 | ||
|
|
e67e9cc647 | ||
|
|
07c4389f6a | ||
|
|
171c5634f2 | ||
|
|
d7bda10e6f | ||
|
|
20bff9003e | ||
|
|
6e0534400b | ||
|
|
17c116a53b | ||
|
|
27b769362b | ||
|
|
b8e7d57ee6 | ||
|
|
ed052c6792 | ||
|
|
5cd2c63320 | ||
|
|
15567a81e9 | ||
|
|
7ab68dcc2c | ||
|
|
30131fd9e4 | ||
|
|
ddcdd545d9 | ||
|
|
d25a399aba | ||
|
|
ee96a44fef | ||
|
|
dda3962249 | ||
|
|
c2743472bc | ||
|
|
e73fd56152 | ||
|
|
04a013f7ee | ||
|
|
b966e3ea07 | ||
|
|
9d53f758d4 | ||
|
|
0689f4e7e1 | ||
|
|
867d515a35 | ||
|
|
381d946808 | ||
|
|
f307300b96 | ||
|
|
d74e7b63ca | ||
|
|
792eec7d4b | ||
|
|
67e93a12e6 | ||
|
|
e188552c1e | ||
|
|
ebb1b6dffb | ||
|
|
25f9bee963 | ||
|
|
6a6fac7f82 | ||
|
|
555b4b48bb | ||
|
|
3522569f97 | ||
|
|
699ea150ce | ||
|
|
e4385adfa9 | ||
|
|
1d46923c47 | ||
|
|
0bf706f438 | ||
|
|
399b7dd682 | ||
|
|
e8f2940024 | ||
|
|
62d5d66d20 | ||
|
|
8b672a159f | ||
|
|
d68212f4ce | ||
|
|
592390dc84 | ||
|
|
d2b2f17b1c | ||
|
|
2c88653a69 | ||
|
|
45b4eb4b3a | ||
|
|
7b93bbe5ff | ||
|
|
d21c0c0dfc | ||
|
|
f5e46967ec | ||
|
|
e0885e2567 | ||
|
|
3b56887267 | ||
|
|
b5d5312c70 | ||
|
|
722b436cad | ||
|
|
bb50466aa9 | ||
|
|
da0395be5e | ||
|
|
9e82f3d7b8 | ||
|
|
67fe0d6bfd | ||
|
|
dddf5aa195 | ||
|
|
92945cafdf | ||
|
|
5cf6617231 | ||
|
|
03c84ff8b5 | ||
|
|
0cf3ade332 | ||
|
|
7c24b6feb0 | ||
|
|
687121ff14 | ||
|
|
e32c584b3b | ||
|
|
64e4416f54 | ||
|
|
ca195887e0 | ||
|
|
f9a5458b94 | ||
|
|
244d072a65 | ||
|
|
d9bc97e012 | ||
|
|
e7491e5182 | ||
|
|
550b8835c6 | ||
|
|
1851721d9a | ||
|
|
603d65a790 | ||
|
|
0cba3f9e3f | ||
|
|
81960a7f05 | ||
|
|
62c4a8c541 | ||
|
|
510d8357ee | ||
|
|
4c6f6f14f3 | ||
|
|
27af57289f | ||
|
|
17aa8c9036 | ||
|
|
c910bda534 | ||
|
|
8d01b96281 | ||
|
|
55f12d5358 | ||
|
|
018c146d72 | ||
|
|
068ac0e992 | ||
|
|
b036129efb | ||
|
|
b3c8eb151a | ||
|
|
e86d584ea7 | ||
|
|
e0226e54a5 | ||
|
|
bde1457524 | ||
|
|
2ad2a79bd0 | ||
|
|
f013bb9d6e | ||
|
|
1b4a723500 | ||
|
|
b5d2af3371 | ||
|
|
4205a34f04 | ||
|
|
e86a9d697c | ||
|
|
f82dabcc75 | ||
|
|
09717d92c5 | ||
|
|
a0d8711d5c | ||
|
|
c24b68481d | ||
|
|
df7fe9750d | ||
|
|
9c22bb4fed | ||
|
|
3803cf2506 | ||
|
|
51a11fbebd | ||
|
|
cc7476c75b | ||
|
|
8a7cedd5b4 | ||
|
|
7e528cf260 | ||
|
|
8a39ff0580 | ||
|
|
710fbbdc08 | ||
|
|
6c52896f99 | ||
|
|
f526df98c7 | ||
|
|
521d74e082 | ||
|
|
225683562b | ||
|
|
9138787f86 | ||
|
|
6ffb9e4d0f | ||
|
|
bced9327b1 | ||
|
|
8120656198 | ||
|
|
941b7cf04c | ||
|
|
1952ef0853 | ||
|
|
d9f2d14e99 | ||
|
|
555d08e8ce | ||
|
|
76009613fe | ||
|
|
651938a322 | ||
|
|
b86011aa94 | ||
|
|
cf240a392b | ||
|
|
28530722bc | ||
|
|
3b304747f2 | ||
|
|
cd3be696dc | ||
|
|
57933736ea | ||
|
|
a46550c250 | ||
|
|
eed3f489c5 | ||
|
|
d268f8c715 | ||
|
|
3cd26cca35 | ||
|
|
6ad303583b | ||
|
|
e527a1843e | ||
|
|
3f8eb70ff3 | ||
|
|
bd7976d005 | ||
|
|
4e1532af3e | ||
|
|
97999aee44 | ||
|
|
6785104c3a | ||
|
|
193c8242e2 | ||
|
|
59eda5e038 | ||
|
|
3c8de8a52c | ||
|
|
d38a5f38e3 | ||
|
|
fe4f85a597 | ||
|
|
0a1af4e61f | ||
|
|
f6e002edbd | ||
|
|
82effbf8e4 | ||
|
|
54096b391f | ||
|
|
39eee12f53 | ||
|
|
bae19b9c3a | ||
|
|
f00e1ebf20 | ||
|
|
939e28d2e1 | ||
|
|
3fbdcab7b0 | ||
|
|
a1d0dba0a5 | ||
|
|
5e41849f69 | ||
|
|
cf56e20be9 | ||
|
|
1b9b2a5f8d | ||
|
|
690b46a59a | ||
|
|
4d33c87b33 | ||
|
|
7cf0c4b16b | ||
|
|
910b643db0 | ||
|
|
0e5ff844b6 | ||
|
|
a9a784f0f0 | ||
|
|
af390f584e | ||
|
|
eff37d74dc | ||
|
|
0ea2da424f | ||
|
|
07e34609e7 | ||
|
|
74c757d0c8 | ||
|
|
e6357a9d6c | ||
|
|
5a047b9d2d | ||
|
|
873ab6cdc7 | ||
|
|
af6736d352 | ||
|
|
d5c1955ea3 | ||
|
|
8bb8131dd4 | ||
|
|
7f4abb5866 | ||
|
|
fbe87cc536 | ||
|
|
74165ace0f | ||
|
|
b576287ac6 | ||
|
|
68d7211735 | ||
|
|
dd0dcace87 | ||
|
|
8f58c39448 | ||
|
|
cee0e2ed42 | ||
|
|
557ce7e006 | ||
|
|
d50521d9b6 | ||
|
|
28aa2329fb | ||
|
|
9707148b86 | ||
|
|
4e739c857b | ||
|
|
363fecc7d6 | ||
|
|
6bbaad59a4 | ||
|
|
497fcf3bbb | ||
|
|
b9ec9f863d | ||
|
|
eafd0b3353 | ||
|
|
a42b6e02ab | ||
|
|
79116e7c7f | ||
|
|
128ff594be | ||
|
|
c821454df8 | ||
|
|
73e8b476fb | ||
|
|
faa95ffb9c | ||
|
|
6a5c4c95b7 | ||
|
|
7141e9d8cf | ||
|
|
d7bbb4261c | ||
|
|
251b16cb62 | ||
|
|
da80c599bf | ||
|
|
ca6a900874 | ||
|
|
5e09e96f7b | ||
|
|
068500df84 | ||
|
|
15949a7e21 | ||
|
|
e635fe3378 | ||
|
|
8276c5db57 | ||
|
|
1d9876a6eb | ||
|
|
6340174927 | ||
|
|
7b031b228e | ||
|
|
ec408940e4 | ||
|
|
bd46fc11f4 | ||
|
|
42e55fe248 | ||
|
|
3a9011b3cb | ||
|
|
e3bf0442e8 | ||
|
|
0413100e2b | ||
|
|
7cad04bbf4 | ||
|
|
c9f0bbf26a | ||
|
|
d91639172a | ||
|
|
5f0cc1becd | ||
|
|
a8bcab7770 | ||
|
|
b1682e4f54 | ||
|
|
2447b34107 | ||
|
|
8126d19682 | ||
|
|
6cc8c81e3d | ||
|
|
e86d812ee6 | ||
|
|
c6f042830f | ||
|
|
2378c0c441 | ||
|
|
7bc53d7bc6 | ||
|
|
7918d6c0d1 | ||
|
|
073e8b897b | ||
|
|
46a3006f4d | ||
|
|
5d578ab9e6 | ||
|
|
f3fe729900 | ||
|
|
a04c157770 | ||
|
|
320ffcb434 | ||
|
|
37a2fb17b5 | ||
|
|
b5936cec54 | ||
|
|
ea61c81cce | ||
|
|
64d71d0d20 | ||
|
|
af08b4c004 | ||
|
|
ea670b6ae6 | ||
|
|
f19e354418 | ||
|
|
b54e3dd243 | ||
|
|
9798e39e49 | ||
|
|
66fd0c19da | ||
|
|
8d6c67206c | ||
|
|
8cf683eb50 | ||
|
|
580a23f3f5 | ||
|
|
29dc46a993 | ||
|
|
93885bad7b | ||
|
|
6e0619c21e | ||
|
|
8c956afabd | ||
|
|
f9fbf62283 | ||
|
|
5f1f536181 | ||
|
|
ba59514464 | ||
|
|
6bb6750be7 | ||
|
|
19a3df6d47 | ||
|
|
87408fd2a9 | ||
|
|
9ee39f45c9 | ||
|
|
a763304386 | ||
|
|
f8ce501b53 | ||
|
|
54d449647c | ||
|
|
552fd39d74 | ||
|
|
01d73f2753 | ||
|
|
75da496128 |
No files matched your search
@@ -52,7 +52,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@@ -75,4 +75,4 @@ runs:
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
sudo apt-get install network-manager
|
||||
flutter test ${{ inputs.test_path }} -d Linux --coverage
|
||||
shell: bash
|
||||
shell: bash
|
||||
@@ -1,126 +1,196 @@
|
||||
# name: Android CI
|
||||
name: Android CI
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - "main"
|
||||
# paths:
|
||||
# - ".github/workflows/mobile_ci.yaml"
|
||||
# - "frontend/**"
|
||||
# - "!frontend/appflowy_tauri/**"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - "main"
|
||||
# paths:
|
||||
# - ".github/workflows/mobile_ci.yaml"
|
||||
# - "frontend/**"
|
||||
# - "!frontend/appflowy_tauri/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
# env:
|
||||
# CARGO_TERM_COLOR: always
|
||||
# FLUTTER_VERSION: "3.22.0"
|
||||
# RUST_TOOLCHAIN: "1.77.2"
|
||||
# CARGO_MAKE_VERSION: "0.36.6"
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
# concurrency:
|
||||
# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# cancel-in-progress: true
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# jobs:
|
||||
# build:
|
||||
# if: github.event.pull_request.draft != true
|
||||
# strategy:
|
||||
# fail-fast: true
|
||||
# matrix:
|
||||
# os: [macos-14]
|
||||
# runs-on: ${{ matrix.os }}
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
# steps:
|
||||
# - name: Check storage space
|
||||
# run: df -h
|
||||
steps:
|
||||
- name: Check storage space
|
||||
run:
|
||||
df -h
|
||||
|
||||
# # the following step is required to avoid running out of space
|
||||
# - name: Maximize build space
|
||||
# if: matrix.os == 'ubuntu-latest'
|
||||
# run: |
|
||||
# sudo rm -rf /usr/share/dotnet
|
||||
# sudo rm -rf /opt/ghc
|
||||
# sudo rm -rf "/usr/local/share/boost"
|
||||
# sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
# sudo docker image prune --all --force
|
||||
# sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
# sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
# sudo rm -rf $ANDROID_HOME/ndk
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
|
||||
# - name: Check storage space
|
||||
# run: df -h
|
||||
- name: Check storage space
|
||||
run: df -h
|
||||
|
||||
# - name: Checkout source code
|
||||
# uses: actions/checkout@v4
|
||||
- name: Checkout appflowy cloud code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: AppFlowy-IO/AppFlowy-Cloud
|
||||
path: AppFlowy-Cloud
|
||||
|
||||
# - uses: actions/setup-java@v4
|
||||
# with:
|
||||
# distribution: temurin
|
||||
# java-version: 11
|
||||
- name: Prepare appflowy cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
# log level
|
||||
cp deploy.env .env
|
||||
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
|
||||
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env
|
||||
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
|
||||
# - name: Install Rust toolchain
|
||||
# id: rust_toolchain
|
||||
# uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
# override: true
|
||||
# profile: minimal
|
||||
- name: Run Docker-Compose
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
else
|
||||
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
|
||||
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version..."
|
||||
# Remove all containers if any exist
|
||||
if [ "$(docker ps -aq)" ]; then
|
||||
docker rm -f $(docker ps -aq)
|
||||
else
|
||||
echo "No containers to remove."
|
||||
fi
|
||||
|
||||
# - name: Install flutter
|
||||
# id: flutter
|
||||
# uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# channel: "stable"
|
||||
# flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
# Remove all volumes if any exist
|
||||
if [ "$(docker volume ls -q)" ]; then
|
||||
docker volume rm $(docker volume ls -q)
|
||||
else
|
||||
echo "No volumes to remove."
|
||||
fi
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
docker ps -a
|
||||
docker compose logs
|
||||
else
|
||||
echo "AppFlowy-Cloud is running with the correct version."
|
||||
fi
|
||||
fi
|
||||
|
||||
# - uses: gradle/gradle-build-action@v3
|
||||
# with:
|
||||
# gradle-version: 7.4.2
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - uses: davidB/rust-cargo-make@v1
|
||||
# with:
|
||||
# version: "0.36.6"
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
# - name: Install prerequisites
|
||||
# working-directory: frontend
|
||||
# run: |
|
||||
# rustup target install aarch64-linux-android
|
||||
# rustup target install x86_64-linux-android
|
||||
# cargo install --force duckscript_cli
|
||||
# cargo install cargo-ndk
|
||||
# if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
# sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
# sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
# sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
# sudo apt-get install gcc-multilib
|
||||
# elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
# vcpkg integrate install
|
||||
# elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
# echo 'do nothing'
|
||||
# fi
|
||||
# cargo make appflowy-flutter-deps-tools
|
||||
# shell: bash
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
# - name: Build AppFlowy
|
||||
# working-directory: frontend
|
||||
# run: |
|
||||
# cargo make --profile development-android appflowy-android-dev-ci
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
gradle-version: 8.10
|
||||
|
||||
# - name: Run integration tests
|
||||
# # https://github.com/ReactiveCircus/android-emulator-runner
|
||||
# uses: reactivecircus/android-emulator-runner@v2
|
||||
# with:
|
||||
# api-level: 32
|
||||
# arch: arm64-v8a
|
||||
# disk-size: 2048M
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# script: flutter test integration_test/runner.dart
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
rustup target install aarch64-linux-android
|
||||
rustup target install x86_64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install cargo-ndk
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
sudo apt-get install gcc-multilib
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile development-android appflowy-core-dev-android
|
||||
cargo make --profile development-android code_generation
|
||||
cd rust-lib
|
||||
cargo clean
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Run integration tests
|
||||
# https://github.com/ReactiveCircus/android-emulator-runner
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 33
|
||||
arch: x86_64
|
||||
disk-size: 2048M
|
||||
working-directory: frontend/appflowy_flutter
|
||||
disable-animations: true
|
||||
force-avd-creation: false
|
||||
target: google_apis
|
||||
script: flutter test integration_test/mobile/cloud/cloud_runner.dart
|
||||
@@ -2,9 +2,9 @@ name: Docker-CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "release/*"]
|
||||
branches: [ "main", "release/*" ]
|
||||
pull_request:
|
||||
branches: ["main", "release/*"]
|
||||
branches: [ "main", "release/*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -25,9 +25,10 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -73,9 +74,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ windows-2019 ]
|
||||
os: [windows-latest]
|
||||
include:
|
||||
- os: windows-2019
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -100,7 +101,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
os: [macos-latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
@@ -122,12 +123,12 @@ jobs:
|
||||
flutter_profile: ${{ matrix.flutter_profile }}
|
||||
|
||||
unit_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -173,7 +174,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -216,11 +217,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
cloud_integration_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -247,9 +248,9 @@ jobs:
|
||||
- name: Run Docker-Compose
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
@@ -261,11 +262,26 @@ jobs:
|
||||
else
|
||||
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
|
||||
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version..."
|
||||
# Remove all containers if any exist
|
||||
if [ "$(docker ps -aq)" ]; then
|
||||
docker rm -f $(docker ps -aq)
|
||||
else
|
||||
echo "No containers to remove."
|
||||
fi
|
||||
|
||||
# Remove all volumes if any exist
|
||||
if [ "$(docker volume ls -q)" ]; then
|
||||
docker volume rm $(docker volume ls -q)
|
||||
else
|
||||
echo "No volumes to remove."
|
||||
fi
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
docker ps -a
|
||||
docker compose logs
|
||||
else
|
||||
echo "AppFlowy-Cloud is running with the correct version."
|
||||
fi
|
||||
@@ -292,7 +308,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@@ -323,14 +339,14 @@ jobs:
|
||||
flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage
|
||||
shell: bash
|
||||
|
||||
# split the integration tests into different machines to minimize the time
|
||||
integration_test_1:
|
||||
needs: [ prepare-linux ]
|
||||
integration_test:
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
@@ -339,76 +355,10 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 1
|
||||
- name: Flutter Integration Test ${{ matrix.test_number }}
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
test_path: integration_test/desktop_runner_1.dart
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
||||
integration_test_2:
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 2
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
test_path: integration_test/desktop_runner_2.dart
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
||||
integration_test_3:
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 3
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
test_path: integration_test/desktop_runner_3.dart
|
||||
test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
pull_request:
|
||||
@@ -16,12 +15,11 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -48,13 +46,13 @@ jobs:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
build-macos:
|
||||
integration-tests:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -85,7 +83,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
rustup target install aarch64-apple-ios-sim
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install cargo-lipo
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
@@ -102,16 +100,20 @@ jobs:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
# - name: Run AppFlowy on simulator
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# run: |
|
||||
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
# pid=$!
|
||||
# sleep 500
|
||||
# kill $pid
|
||||
# continue-on-error: true
|
||||
- name: Run AppFlowy on simulator
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
pid=$!
|
||||
sleep 500
|
||||
kill $pid
|
||||
continue-on-error: true
|
||||
|
||||
# enable it again if the 12 mins timeout is fixed
|
||||
# - name: Run integration tests
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
# Integration tests
|
||||
- name: Run integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
# The integration tests are flaky and sometimes fail with "Connection timed out":
|
||||
# Don't block the CI. If the tests fail, the CI will still pass.
|
||||
# Instead, we're using Code Magic to re-run the tests to check if they pass.
|
||||
continue-on-error: true
|
||||
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Mobile-CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch to build"
|
||||
required: true
|
||||
default: "main"
|
||||
workflow_id:
|
||||
description: "Codemagic workflow ID"
|
||||
required: true
|
||||
default: "ios-workflow"
|
||||
type: choice
|
||||
options:
|
||||
- ios-workflow
|
||||
- android-workflow
|
||||
|
||||
env:
|
||||
CODEMAGIC_API_TOKEN: ${{ secrets.CODEMAGIC_API_TOKEN }}
|
||||
APP_ID: "6731d2f427e7c816080c3674"
|
||||
|
||||
jobs:
|
||||
trigger-mobile-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Codemagic Build
|
||||
id: trigger_build
|
||||
run: |
|
||||
RESPONSE=$(curl -X POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "x-auth-token: $CODEMAGIC_API_TOKEN" \
|
||||
--data '{
|
||||
"appId": "${{ env.APP_ID }}",
|
||||
"workflowId": "${{ github.event.inputs.workflow_id }}",
|
||||
"branch": "${{ github.event.inputs.branch }}"
|
||||
}' \
|
||||
https://api.codemagic.io/builds)
|
||||
|
||||
BUILD_ID=$(echo $RESPONSE | jq -r '.buildId')
|
||||
echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT
|
||||
echo "build_id=$BUILD_ID"
|
||||
|
||||
- name: Wait for build and check status
|
||||
id: check_status
|
||||
run: |
|
||||
while true; do
|
||||
curl -X GET \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "x-auth-token: $CODEMAGIC_API_TOKEN" \
|
||||
https://api.codemagic.io/builds/${{ steps.trigger_build.outputs.build_id }} > /tmp/response.json
|
||||
|
||||
RESPONSE_WITHOUT_COMMAND=$(cat /tmp/response.json | jq 'walk(if type == "object" and has("subactions") then .subactions |= map(del(.command)) else . end)')
|
||||
STATUS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.build.status')
|
||||
|
||||
if [ "$STATUS" = "finished" ]; then
|
||||
SUCCESS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.success')
|
||||
BUILD_URL=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.buildUrl')
|
||||
echo "status=$STATUS" >> $GITHUB_OUTPUT
|
||||
echo "success=$SUCCESS" >> $GITHUB_OUTPUT
|
||||
echo "build_url=$BUILD_URL" >> $GITHUB_OUTPUT
|
||||
break
|
||||
elif [ "$STATUS" = "failed" ]; then
|
||||
echo "status=failed" >> $GITHUB_OUTPUT
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Slack Notification
|
||||
uses: 8398a7/action-slack@v3
|
||||
if: always()
|
||||
with:
|
||||
status: ${{ steps.check_status.outputs.success == 'true' && 'success' || 'failure' }}
|
||||
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
|
||||
text: |
|
||||
Mobile CI Build Result
|
||||
Branch: ${{ github.event.inputs.branch }}
|
||||
Workflow: ${{ github.event.inputs.workflow_id }}
|
||||
Build URL: ${{ steps.check_status.outputs.build_url }}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
|
||||
@@ -6,8 +6,8 @@ on:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -73,8 +73,8 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
vcpkg integrate install
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build Windows app
|
||||
working-directory: frontend
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
|
||||
- { target: x86_64-apple-darwin, os: macos-13, extra-build-args: "" }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -158,8 +158,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
@@ -256,8 +256,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
@@ -338,7 +338,7 @@ jobs:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-22.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
@@ -368,10 +368,10 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install -y alien libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y alien libnotify-dev
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
|
||||
- name: Install gcc-aarch64-linux-gnu
|
||||
|
||||
@@ -15,84 +15,21 @@ on:
|
||||
- "main"
|
||||
- "develop"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CLOUD_VERSION: 0.8.3-amd64
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
self-hosted-job:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Appflowy Cloud
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: AppFlowy-IO/AppFlowy-Cloud
|
||||
path: AppFlowy-Cloud
|
||||
|
||||
- name: Prepare Appflowy Cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
cp deploy.env .env
|
||||
sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
|
||||
- name: Ensure AppFlowy-Cloud is Running with Correct Version
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
else
|
||||
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
|
||||
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
else
|
||||
echo "AppFlowy-Cloud is running with the correct version."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run rust-lib tests
|
||||
working-directory: frontend/rust-lib
|
||||
env:
|
||||
RUST_LOG: info
|
||||
RUST_BACKTRACE: 1
|
||||
af_cloud_test_base_url: http://localhost
|
||||
af_cloud_test_ws_url: ws://localhost/ws/v1
|
||||
af_cloud_test_gotrue_url: http://localhost/gotrue
|
||||
run: |
|
||||
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
|
||||
|
||||
- name: rustfmt rust-lib
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: frontend/rust-lib/
|
||||
|
||||
- name: clippy rust-lib
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
working-directory: frontend/rust-lib
|
||||
|
||||
ubuntu-job:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set timezone for action
|
||||
uses: szenius/set-timezone@v2.0
|
||||
with:
|
||||
timezoneLinux: "US/Pacific"
|
||||
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
@@ -129,36 +66,37 @@ jobs:
|
||||
run: |
|
||||
cp deploy.env .env
|
||||
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env
|
||||
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env
|
||||
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
|
||||
- name: Ensure AppFlowy-Cloud is Running with Correct Version
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
# Remove all containers if any exist
|
||||
if [ "$(docker ps -aq)" ]; then
|
||||
docker rm -f $(docker ps -aq)
|
||||
else
|
||||
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
|
||||
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
else
|
||||
echo "AppFlowy-Cloud is running with the correct version."
|
||||
fi
|
||||
echo "No containers to remove."
|
||||
fi
|
||||
|
||||
# Remove all volumes if any exist
|
||||
if [ "$(docker volume ls -q)" ]; then
|
||||
docker volume rm $(docker volume ls -q)
|
||||
else
|
||||
echo "No volumes to remove."
|
||||
fi
|
||||
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
docker ps -a
|
||||
docker compose logs
|
||||
|
||||
- name: Run rust-lib tests
|
||||
working-directory: frontend/rust-lib
|
||||
env:
|
||||
|
||||
@@ -10,8 +10,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -40,8 +40,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
name: Tauri-CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tauri2_ci.yaml"
|
||||
- "frontend/rust-lib/**"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
CI: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# tauri-build-self-hosted:
|
||||
# if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
# runs-on: self-hosted
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: install frontend dependencies
|
||||
# working-directory: frontend/appflowy_web_app
|
||||
# run: |
|
||||
# mkdir dist
|
||||
# pnpm install
|
||||
# cd src-tauri && cargo build
|
||||
#
|
||||
# - name: test and lint
|
||||
# working-directory: frontend/appflowy_web_app
|
||||
# run: |
|
||||
# pnpm run lint:tauri
|
||||
#
|
||||
# - uses: tauri-apps/tauri-action@v0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# tauriScript: pnpm tauri
|
||||
# projectPath: frontend/appflowy_web_app
|
||||
# args: "--debug"
|
||||
|
||||
tauri-build-ubuntu:
|
||||
#if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: install tauri deps tools
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cd src-tauri && cargo build
|
||||
|
||||
- name: test and lint
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint:tauri
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_web_app
|
||||
args: "--debug"
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Tauri-CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/tauri
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- name: frontend tests and linting
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
||||
@@ -1,153 +0,0 @@
|
||||
name: Publish Tauri Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'The branch to release'
|
||||
required: true
|
||||
default: 'main'
|
||||
version:
|
||||
description: 'The version to release'
|
||||
required: true
|
||||
default: '0.0.0'
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
jobs:
|
||||
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: windows-latest
|
||||
args: "--verbose"
|
||||
target: "windows-x86_64"
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-20.04
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- uses: tauri-apps/tauri-action@dev
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
|
||||
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
|
||||
CI: true
|
||||
with:
|
||||
args: ${{ matrix.settings.args }}
|
||||
appVersion: ${{ github.event.inputs.version }}
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
|
||||
- name: Upload EXE package(windows only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.exe
|
||||
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
|
||||
|
||||
- name: Upload DMG package(macos only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'macos-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.dmg
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Web-CI
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/web2_ci.yaml"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
web-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Run lint check
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint
|
||||
|
||||
- name: build and analyze
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run analyze >> analyze-size.txt
|
||||
- name: Upload analyze-size.txt
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: analyze-size.txt
|
||||
path: frontend/appflowy_web_app/analyze-size.txt
|
||||
retention-days: 30
|
||||
- name: Upload stats.html
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stats.html
|
||||
path: frontend/appflowy_web_app/dist/stats.html
|
||||
retention-days: 30
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Web Code Coverage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/web2_ci.yaml"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space (ubuntu only)
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# Install pnpm dependencies, cache them correctly
|
||||
# and run all Cypress tests
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
working-directory: frontend/appflowy_web_app
|
||||
component: true
|
||||
build: pnpm run build
|
||||
start: pnpm run start
|
||||
browser: chrome
|
||||
|
||||
- name: Jest run
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run test:unit
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
|
||||
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
|
||||
flags: appflowy_web_app
|
||||
name: frontend/appflowy_web_app
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
+247
-2
@@ -1,4 +1,249 @@
|
||||
# Release Notes
|
||||
## Version 0.8.9 - 16/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Supported pasting a link as a mention, providing a more condensed visualization of linked content
|
||||
- Supported converting between link formats (e.g. transforming a mention into a bookmark)
|
||||
- Improved the link editing experience with enhanced UX
|
||||
- Added OTP (One-Time Password) support for sign-in authentication
|
||||
- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where properties were not displaying in the row detail page
|
||||
- Fixed a bug where Undo didn't work in the row detail page
|
||||
- Fixed an issue where blocks didn't grow when the grid got bigger
|
||||
- Fixed several bugs related to AI writers
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Added sign-in with OTP (One-Time Password)
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where the slash menu sometimes failed to display
|
||||
- Updated the mention page block to handle page selection with more context.
|
||||
|
||||
## Version 0.8.8 - 01/04/2025
|
||||
### New Features
|
||||
- Added support for selecting AI models in AI writer
|
||||
- Revamped link menu in toolbar
|
||||
- Added support for using ":" to add emojis in documents
|
||||
- Passed the history of past AI prompts and responses to AI writer
|
||||
### Bug Fixes
|
||||
- Improved AI writer scrolling user experience
|
||||
- Fixed issue where checklist items would disappear during reordering
|
||||
- Fixed numbered lists generated by AI to maintain the same index as the input
|
||||
|
||||
## Version 0.8.7 - 18/03/2025
|
||||
### New Features
|
||||
- Made local AI free and integrated with Ollama
|
||||
- Supported nested lists within callout and quote blocks
|
||||
- Revamped the document's floating toolbar and added Turn Into
|
||||
- Enabled custom icons in callout blocks
|
||||
### Bug Fixes
|
||||
- Fixed occasional incorrect positioning of the slash menu
|
||||
- Improved AI Chat and AI Writers with various bug fixes
|
||||
- Adjusted the columns block to match the width of the editor
|
||||
- Fixed a potential segfault caused by infinite recursion in the trash view
|
||||
- Resolved an issue where the first added cover might be invisible
|
||||
- Fixed adding cover images via Unsplash
|
||||
|
||||
## Version 0.8.6 - 06/03/2025
|
||||
### Bug Fixes
|
||||
- Fix the incorrect title positioning when adjusting the document width setting
|
||||
- Enhance the user experience of the icon color picker for smoother interactions
|
||||
- Add missing icons to the database to ensure completeness and consistency
|
||||
- Resolve the issue with links not functioning correctly on Linux systems
|
||||
- Improve the outline feature to work seamlessly within columns
|
||||
- Center the bulleted list icon within columns for better visual alignment
|
||||
- Enable dragging blocks under tables in the second column to enhance flexibility
|
||||
- Disable the AI writer feature within tables to prevent conflicts and improve usability
|
||||
- Automatically enable the header row when converting content from Markdown to ensure proper formatting
|
||||
- Use the "Undo" function to revert the auto-formatting
|
||||
|
||||
## Version 0.8.5 - 04/03/2025
|
||||
### New Features
|
||||
- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu
|
||||
- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more
|
||||
- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen
|
||||
### Bug Fixes
|
||||
- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document
|
||||
- Fixed a bug preventing the relation field in databases from opening
|
||||
- Fixed an issue where links in documents were unclickable on Linux
|
||||
|
||||
## Version 0.8.4 - 18/02/2025
|
||||
### New Features
|
||||
- Switch AI mode on mobile
|
||||
- Support locking page
|
||||
- Support uploading svg file as icon
|
||||
- Support the slash, at, and plus menus on mobile
|
||||
### Bug Fixes
|
||||
- Gallery not rendering in row page
|
||||
- Save image should not copy the image (mobile)
|
||||
- Support exporting more content to markdown
|
||||
|
||||
## Version 0.8.2 - 23/01/2025
|
||||
### New Features
|
||||
- Customized database view icons
|
||||
- Support for uploading images as custom icons
|
||||
- Enabled selecting multiple AI messages to save into a document
|
||||
- Added the ability to scale the app's display size on mobile
|
||||
- Support for pasting image links without file extensions
|
||||
### Bug Fixes
|
||||
- Fixed an issue where pasting tables from other apps wasn't working
|
||||
- Fixed homepage URL issues in Settings
|
||||
- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page
|
||||
|
||||
## Version 0.8.1 - 14/01/2025
|
||||
### New Features
|
||||
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
|
||||
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
|
||||
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
|
||||
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
|
||||
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
|
||||
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
|
||||
### Bug Fixes
|
||||
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
|
||||
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
|
||||
|
||||
## Version 0.8.0 - 06/01/2025
|
||||
### Bug Fixes
|
||||
- Fixed error displaying in the page style menu
|
||||
- Fixed filter logic in the icon picker
|
||||
- Fixed error displaying in the Favorite/Recent page
|
||||
- Fixed the color picker displaying when tapping down
|
||||
- Fixed icons not being supported in subpage blocks
|
||||
- Fixed recent icon functionality in the space icon menu
|
||||
- Fixed "Insert Below" not auto-scrolling the table
|
||||
- Fixed a to-do item with an emoji automatically creating a soft break
|
||||
- Fixed header row/column tap areas being too small
|
||||
- Fixed simple table alignment not working for items that wrap
|
||||
- Fixed web content reverting after removing the inline code format on desktop
|
||||
- Fixed inability to make changes to a row or column in the table when opening a new tab
|
||||
- Fixed changing the language to CKB-KU causing a gray screen on mobile
|
||||
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
<img width="1068" alt="image" src="https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac" />
|
||||
|
||||
- Meet Simple Table 2.0:
|
||||
- Insert a list into a table cell
|
||||
- Insert images, quotes, callouts, and code blocks into a table cell
|
||||
- Drag to move rows or columns
|
||||
- Toggle header rows or columns on/off
|
||||
- Distribute columns evenly
|
||||
- Adjust to page width
|
||||
- Enjoy a new UI/UX for a seamless experience
|
||||
- Revamped mention page interactions in AI Chat
|
||||
- Improved AppFlowy AI service
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an error when opening files in the database in local mode
|
||||
- Fixed arrow up/down navigation not working for selecting a language in Code Block
|
||||
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
|
||||
|
||||
## Version 0.7.7 - 09/12/2024
|
||||
### Bug Fixes
|
||||
- Fixed sidebar menu resize regression
|
||||
- Fixed AI chat loading issues
|
||||
- Fixed inability to open local files in database
|
||||
- Fixed mentions remaining in notifications after removal from document
|
||||
- Fixed event card closing when clicking on empty space
|
||||
- Fixed keyboard shortcut issues
|
||||
|
||||
## Version 0.7.6 - 03/12/2024
|
||||
### New Features
|
||||
- Revamped the simple table UI
|
||||
- Added support for capturing images from camera on mobile
|
||||
### Bug Fixes
|
||||
- Improved markdown rendering capabilities in AI writer
|
||||
- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line
|
||||
- Fixed an issue where creating a document from slash menu could insert content at incorrect position
|
||||
|
||||
## Version 0.7.5 - 25/11/2024
|
||||
### Bug Fixes
|
||||
- Improved chat response parsing
|
||||
- Fixed toggle list icon direction for RTL mode
|
||||
- Fixed cross blocks formatting not reflecting in float toolbar
|
||||
- Fixed unable to click inside the toggle list to create a new paragraph
|
||||
- Fixed open file error 50 on macOS
|
||||
- Fixed upload file exceed limit error
|
||||
|
||||
## Version 0.7.4 - 19/11/2024
|
||||
### New Features
|
||||
- Support uploading WebP and BMP images
|
||||
- Support managing workspaces on mobile
|
||||
- Support adding toggle headings on mobile
|
||||
- Improve the AI chat page UI
|
||||
### Bug Fixes
|
||||
- Optimized the workspace menu loading performance
|
||||
- Optimized tab switching performance
|
||||
- Fixed searching issues in Document page
|
||||
|
||||
## Version 0.7.3 - 07/11/2024
|
||||
### New Features
|
||||
- Enable custom URLs for published pages
|
||||
- Support toggling headings
|
||||
- Create a subpage by typing in the document
|
||||
- Turn selected blocks into a subpage
|
||||
- Add a manual date picker for the Date property
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the workspace owner was unable to delete spaces created by others
|
||||
- Fixed cursor height inconsistencies with text height
|
||||
- Fixed editing issues in Kanban cards
|
||||
- Fixed an issue preventing images or files from being dropped into empty paragraphs
|
||||
|
||||
## Version 0.7.2 - 22/10/2024
|
||||
### New Features
|
||||
- Copy link to block
|
||||
- Support turn into in document
|
||||
- Enable sharing links and publishing pages on mobile
|
||||
- Enable drag and drop in row documents
|
||||
- Right-click on page in sidebar to open more actions
|
||||
- Create new subpage in document using `+` character
|
||||
- Allow reordering checklist item
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed issue with inability to cancel inline code format in French IME
|
||||
- Fixed delete with Shift or Ctrl shortcuts not working in documents
|
||||
- Fixed the issues with incorrect time zone being used in filters.
|
||||
|
||||
## Version 0.7.1 - 07/10/2024
|
||||
### New Features
|
||||
- Copy link to share and open it in a browser
|
||||
- Enable the ability to edit the page title within the body of the document
|
||||
- Filter by last modified, created at, or a date range
|
||||
- Allow customization of database property icons
|
||||
- Support CTRL/CMD+X to delete the current line when the selection is collapsed in the document
|
||||
- Support window tiling on macOS
|
||||
- Add filters to grid views on mobile
|
||||
- Create and manage workspaces on mobile
|
||||
- Automatically convert property types for imported CSV files
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed calculations with filters applied
|
||||
- Fixed issues with importing data folders into a cloud account
|
||||
- Fixed French IME backtick issues
|
||||
- Fixed selection gesture bugs on mobile
|
||||
|
||||
## Version 0.7.0 - 19/09/2024
|
||||
### New Features
|
||||
- Support reordering blocks in document with drag and drop
|
||||
@@ -42,7 +287,7 @@
|
||||
- Fixed the inability to edit group names on Kanban boards
|
||||
- Made error codes more user-friendly
|
||||
- Added leading zeros to day and month in date format
|
||||
|
||||
|
||||
## Version 0.6.8 - 22/08/2024
|
||||
### New Features
|
||||
- Enabled viewing data inside a database record on mobile.
|
||||
@@ -872,4 +1117,4 @@ Bug fixes and improvements
|
||||
- Increased height of action
|
||||
- CPU performance issue
|
||||
- Fix potential data parser error
|
||||
- More foundation work for online collaboration
|
||||
- More foundation work for online collaboration
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<b>
|
||||
<a href="https://www.appflowy.io">AppFlowy.IO</a><br>
|
||||
<a href="https://www.appflowy.com">AppFlowy</a><br>
|
||||
</b>
|
||||
⭐️ The Open Source Alternative To Notion ⭐️ <br>
|
||||
</h1>
|
||||
@@ -18,18 +18,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.appflowy.io"><b>Website</b></a> •
|
||||
<a href="https://www.appflowy.com"><b>Website</b></a> •
|
||||
<a href="https://forum.appflowy.io/"><b>Forum</b></a> •
|
||||
<a href="https://discord.gg/9Q2xaN37tV"><b>Discord</b></a> •
|
||||
<a href="https://www.reddit.com/r/AppFlowy"><b>Reddit</b></a> •
|
||||
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
|
||||
|
||||
<br></br>
|
||||
<p align="center" >
|
||||
@@ -42,11 +42,13 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
## User Installation
|
||||
|
||||
- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
|
||||
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
|
||||
- Other
|
||||
channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
|
||||
- Available on
|
||||
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
|
||||
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
|
||||
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
|
||||
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
|
||||
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
|
||||
not supported
|
||||
- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview)
|
||||
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
|
||||
|
||||
## Built With
|
||||
@@ -61,32 +63,41 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
|
||||
## Getting Started with development
|
||||
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific
|
||||
development instructions
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
|
||||
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
|
||||
|
||||
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
|
||||
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
|
||||
If you'd like to propose a feature, submit a feature
|
||||
request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
|
||||
If you'd like to report a bug, submit a bug
|
||||
report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
|
||||
|
||||
## **Releases**
|
||||
|
||||
Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.
|
||||
Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
|
||||
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make
|
||||
are **greatly appreciated**. Please look
|
||||
at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy)
|
||||
for details.
|
||||
|
||||
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
|
||||
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
|
||||
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
|
||||
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
|
||||
the community, **Congratulations!** You are now an official contributor to AppFlowy.
|
||||
|
||||
## Translations 🌎🗺
|
||||
|
||||
[](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
|
||||
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use
|
||||
the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or
|
||||
run `npx inlang machine translate` to add missing translations.
|
||||
|
||||
## Join the community to build AppFlowy together
|
||||
|
||||
@@ -96,16 +107,30 @@ To add translations, you can manually edit the JSON translation files in `/front
|
||||
|
||||
## Why Are We Building This?
|
||||
|
||||
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
|
||||
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and
|
||||
functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.
|
||||
These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative
|
||||
workplace management tools also have their constraints.
|
||||
|
||||
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
|
||||
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have
|
||||
led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates
|
||||
from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a
|
||||
proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to
|
||||
come up with a one-size fits all solution in such a fragmented market.
|
||||
|
||||
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
|
||||
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,
|
||||
in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is
|
||||
a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention
|
||||
the speed and native experience. The same may apply to individual users as well.
|
||||
|
||||
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
|
||||
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs
|
||||
well.
|
||||
|
||||
- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
|
||||
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
|
||||
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to
|
||||
enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy
|
||||
your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term
|
||||
maintainability.
|
||||
|
||||
We decided to achieve this mission by upholding the three most fundamental values:
|
||||
|
||||
@@ -113,16 +138,20 @@ We decided to achieve this mission by upholding the three most fundamental value
|
||||
- Reliable native experience
|
||||
- Community-driven extensibility
|
||||
|
||||
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
|
||||
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority
|
||||
doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the
|
||||
knowledge and wheels of making complex workplace management tools while enabling people and businesses to create
|
||||
beautiful things on their own by equipping them with a versatile toolbox of building blocks.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
|
||||
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for
|
||||
more information.
|
||||
|
||||
## Acknowledgements
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to these amazing projects which help power AppFlowy.IO:
|
||||
Special thanks to these amazing projects which help power AppFlowy:
|
||||
|
||||
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
|
||||
- [cargo-make](https://github.com/sagiegurari/cargo-make)
|
||||
- [contrib.rocks](https://contrib.rocks)
|
||||
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
|
||||
@@ -0,0 +1,47 @@
|
||||
workflows:
|
||||
ios-workflow:
|
||||
name: iOS Workflow
|
||||
instance_type: mac_mini_m2
|
||||
max_build_duration: 30
|
||||
environment:
|
||||
flutter: 3.27.4
|
||||
xcode: latest
|
||||
cocoapods: default
|
||||
|
||||
scripts:
|
||||
- name: Build Flutter
|
||||
script: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source "$HOME/.cargo/env"
|
||||
rustc --version
|
||||
cargo --version
|
||||
|
||||
cd frontend
|
||||
|
||||
rustup target install aarch64-apple-ios-sim
|
||||
cargo install --force cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install --force cargo-lipo
|
||||
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
|
||||
cargo make --profile development-ios-arm64-sim code_generation
|
||||
|
||||
- name: iOS integration tests
|
||||
script: |
|
||||
cd frontend/appflowy_flutter
|
||||
flutter emulators --launch apple_ios_simulator
|
||||
flutter -d iPhone test integration_test/runner.dart
|
||||
|
||||
artifacts:
|
||||
- build/ios/ipa/*.ipa
|
||||
- /tmp/xcodebuild_logs/*.log
|
||||
- flutter_drive.log
|
||||
|
||||
publishing:
|
||||
email:
|
||||
recipients:
|
||||
- lucas.xu@appflowy.io
|
||||
notify:
|
||||
success: true
|
||||
failure: true
|
||||
Vendored
+123
-138
@@ -1,140 +1,125 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
{
|
||||
// https://tauri.app/v1/guides/debugging/vs-code
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "AF-tauri: Debug backend",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
|
||||
"--no-default-features"
|
||||
]
|
||||
},
|
||||
"preLaunchTask": "AF: Tauri UI Dev",
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
-45
@@ -245,51 +245,6 @@
|
||||
"problemMatcher": [],
|
||||
"detail": "appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Build",
|
||||
"type": "shell",
|
||||
"command": "pnpm run build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Dev",
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"command": "pnpm sync:i18n && pnpm run dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean",
|
||||
"type": "shell",
|
||||
"command": "cargo make tauri_clean",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean + Dev",
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Tauri Clean",
|
||||
"AF: Tauri UI Dev"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri ESLint",
|
||||
"type": "shell",
|
||||
"command": "npx eslint --fix src",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Env File",
|
||||
"type": "shell",
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.7.1"
|
||||
APPFLOWY_VERSION = "0.9.0"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
- "packages/**/*.dart"
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
- require_trailing_commas
|
||||
|
||||
@@ -51,8 +31,5 @@ linter:
|
||||
- sort_constructors_first
|
||||
- unawaited_futures
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
@@ -53,7 +53,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="appflowy-flutter" />
|
||||
<!-- <data android:host="login-callback" /> -->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!--
|
||||
@@ -44,13 +43,16 @@
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||
-->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Permission to read files from external storage (outside application container).
|
||||
As of Android 12 this permission no longer has any effect. Instead use the
|
||||
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<!-- Permissions to read media files. -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<queries>
|
||||
@@ -65,4 +67,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,874 @@
|
||||
{
|
||||
"prompts": [
|
||||
{
|
||||
"id": "builtin_prompt_1",
|
||||
"name": "Linux terminal",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so.\nThis is my first command:\n\n[command]",
|
||||
"example": "ls -l",
|
||||
"sampleResponse": "```\ntotal 8\ndrwxr-xr-x 2 alice alice 4096 Apr 12 12:34 alice\ndrwxr-xr-x 3 bob bob 4096 Apr 12 12:35 bob\n```"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_2",
|
||||
"name": "Cover Letter Writer and Advisor",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a career advisor, who can assist in crafting a compelling cover letter for job applications. Share insights on how to structure the letter, highlight relevant skills and experiences, and express enthusiasm for the role. Offer advice on how to tailor the letter to the specific job description, company culture, and industry standards. Provide tips on maintaining a professional tone, proofreading for errors, and making a memorable impression on hiring managers. My first request is \n\n[request]",
|
||||
"example": "Help me write a cover letter for a software engineer position at a tech startup, focusing on my coding skills, problem-solving ability, and experience with agile development methodologies."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_3",
|
||||
"name": "Business Plan Consultant",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a business plan consultant, who can guide me through the process of creating a comprehensive and persuasive business plan for a new startup or existing business. Discuss key elements to include in the plan, such as executive summary, company overview, market analysis, marketing strategies, organizational structure, and financial projections. Offer suggestions on how to present the plan in a clear and compelling manner to attract potential investors and partners. My first request is:\n\n[request]",
|
||||
"example": "Help me outline a business plan for an online fitness coaching platform targeting busy professionals."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_4",
|
||||
"name": "Niche Identifier",
|
||||
"category": "business",
|
||||
"content": "As an online business consultant, your task is to help an aspiring entrepreneur identify 5 profitable niches for starting an online business. Consider factors such as market demand, competition, target audience, and potential for growth. For each niche, provide a brief description of the products or services that could be offered, the target customer demographics, and the potential marketing strategies to reach them. Suggest low-cost ways to validate the niche ideas and test their viability before investing significant time or money.",
|
||||
"example": "Help me create a month-long content calendar for Instagram focused on promoting a new skincare line."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_5",
|
||||
"name": "Chief Executive Officer",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a CEO, who can provide strategic insights into running a successful organization. Share advice on setting the company's vision and mission, building a high-performing team, managing resources, and driving growth. Offer guidance on making tough decisions, dealing with risks and crises, and maintaining stakeholder relationships. Also, discuss the responsibilities of a CEO in ensuring ethical conduct and corporate social responsibility. My first request is:\n\n[request]",
|
||||
"example": "Provide a strategic plan for a startup tech company in its first year of operation, focusing on key objectives, potential challenges, and growth strategies."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_6",
|
||||
"name": "Accountant",
|
||||
"category": "business",
|
||||
"content": "I want you to act as an accountant and come up with creative ways to manage finances. You’ll need to consider budgeting, investment strategies and risk management when creating a financial plan for your client. In some cases, you may also need to provide advice on taxation laws and regulations in order to help them maximize their profits. My first suggestion request is:\n\n[request].",
|
||||
"example": "Create a financial plan for a small business that focuses on cost savings and long-term investments"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_7",
|
||||
"name": "Creative Branding Strategist",
|
||||
"category": "business",
|
||||
"content": "You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business’s values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts. Reply in English using professional tone for everyone.",
|
||||
"example": "Help me create a branding strategy for a new organic skincare line targeting eco-conscious consumers."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_8",
|
||||
"name": "Emoji-fy",
|
||||
"category": "other",
|
||||
"content": "Your task is to take the following plain text message provided and convert it into an expressive, emoji-rich message that conveys the same meaning and intent. Replace key words and phrases with relevant emojis wherever possible to add visual interest and emotion. Use emojis creatively but ensure the message remains clear and easy to understand. Do not alter the core message or add new information.\n\n[message]",
|
||||
"sampleResponse": "🍎’s goal isn’t just to 💰. Our 🎯 is to 🎨 design, 🔧 develop, and 🚀 bring to market outstanding products... We trust that, as a result, people will ❤️ them, and as another bonus, we’ll earn some 💵. But we’re absolutely crystal clear about what our 🎯 are."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_9",
|
||||
"name": "Explain like I’m 5",
|
||||
"category": "learning",
|
||||
"content": "Explain [concept or question, e.g., What is gravity?, What does money do?, How do plants grow?] in a way that a 5-year-old would understand. Use very simple language, short sentences, and relatable examples from everyday life (like toys, animals, food, or playground activities). Avoid technical jargon and aim to make the explanation fun and clear.\n\nIf helpful, include a quick story or analogy to illustrate the idea. You can end the explanation with a cheerful summary or a fun fact to keep it engaging.",
|
||||
"sampleResponse": "The internet is like a big library where you can find lots of books, pictures, and videos. But instead of going to the library, you can sit at home and look at everything on a computer or tablet. It’s like magic!"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_10",
|
||||
"name": "Social Media Master",
|
||||
"category": "marketing",
|
||||
"content": "Create an engaging and upbeat social media post for [platform, e.g., Instagram/Twitter/LinkedIn] about [topic or event, e.g., product launch, personal milestone, motivational message]. The tone should be [desired tone, e.g., energetic, friendly, professional] and include [specific elements, e.g., a call to action, hashtags, emojis]. Make it appealing to [target audience, e.g., young professionals, fitness enthusiasts, small business owners].",
|
||||
"sampleResponse": "🌟 Exciting news! 🌟 We’re thrilled to announce the launch of our new product, the Eco-Friendly Water Bottle! 💧🌍 It’s not just a bottle; it’s a step towards a greener planet. Join us in making a difference! 💪✨\n\n👉 Click the link in our bio to learn more and grab yours today! #EcoFriendly #SustainableLiving #HydrationGoals"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_11",
|
||||
"name": "Prompt Enhancer",
|
||||
"category": "other",
|
||||
"content": "Act as a Prompt Enhancer AI that takes user-input prompts and transforms them into more engaging, detailed, and thought-provoking questions. Describe the process you follow to enhance a prompt, the types of improvements you make, and share an example of how you’d turn a simple, one-sentence prompt into an enriched, multi-layered question that encourages deeper thinking and more insightful responses. Reply in English using professional tone for everyone."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_12",
|
||||
"name": "Learning with Historical Figures",
|
||||
"category": "learning",
|
||||
"content": "Pretend you are [famous figure] explaining [topic] to me. Let’s have a dialogue where I can ask questions to understand better.",
|
||||
"sampleResponse": "Albert Einstein: \"Ah, yes, the theory of relativity—one of the most exciting and profound ideas in physics! It’s all about how space and time are linked together. You see, before we understood relativity, people thought of space as just empty ‘space’ and time as a separate thing. But I showed that they are actually connected in a single fabric called 'space-time.' Imagine this: if you stretch a rubber sheet, it bends. Well, the Earth and other massive objects bend space-time in a similar way!\""
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_13",
|
||||
"name": "Salesperson",
|
||||
"category": "marketing",
|
||||
"content": "I want you to act as a salesperson. Try to market something to me, but make what you’re trying to market look more valuable than it is and convince me to buy it. Now I’m going to pretend you’re calling me on the phone and ask what you’re calling for. Hello, what did you call for? Reply in English using professional tone for everyone."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_14",
|
||||
"name": "Marketing Funnel Framework",
|
||||
"category": "marketing",
|
||||
"content": "Using the 'Marketing Funnel' framework, please write a marketing campaign outline that targets [awareness consideration conversion] stage of the customer journey and aligns with the goals of each stage. Highlight the [features] of our [product/service] and explain how it can [solve a problem] or [achieve a goal] for [ideal customer persona]."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_15",
|
||||
"name": "Email Marketing Campaign",
|
||||
"category": "marketing",
|
||||
"content": "As an experienced email marketer, your goal is to create an engaging email campaign to promote [insert product/service/event]. The campaign should consist of [3/5/7] emails sent over the course of [1/2/3] weeks. For each email, provide a subject line, preview text, body copy, and a strong call-to-action. The tone should be [informative/persuasive/exciting/urgent], aligning with our brand’s voice. Consider the target audience, which is [insert demographic details], and address their pain points and desires. Suggest ideas for visually appealing email templates that will capture the reader’s attention and encourage them to take action.\n\n[Text here]",
|
||||
"sampleResponse": "📧 Campaign: FocusFlow Launch Email Series\nEmail 1 – Subject:\n\n“Take back your time – Meet FocusFlow”\nPreview Text: Discover how to finally beat distractions and stay on task.\n\nBody:\nAre constant distractions eating away at your productivity? You're not alone.\nThat’s why we created FocusFlow – the all-in-one app that helps busy professionals like you prioritize tasks, block interruptions, and work with intention.\nIn just minutes a day, FocusFlow helps you reclaim your schedule and finally get things done.\n\nCTA:\n👉 Try FocusFlow Free Today\nEmail 2 – Subject:\n\n“3 reasons you're not getting work done (and how to fix them)”\nPreview Text: Distractions, disorganization, and mental fatigue—FocusFlow tackles them all.\n\nBody:\nIt’s not that you aren’t trying. You just need better tools.\nHere’s why you’re struggling—and how FocusFlow helps:\n✅ No more tab overload: Everything’s in one place\n✅ Built-in focus timer boosts concentration\n✅ Smart task suggestions eliminate decision fatigue\n\nCTA:\n🚀 Boost Productivity Now\nEmail 3 – Subject:\n\n“What top performers do differently”\nPreview Text: Want to work like the pros? Here’s their secret weapon.\n\nBody:\nHigh achievers don’t rely on willpower—they rely on systems.\nFocusFlow helps you set routines, stick to them, and stay aligned with your goals.\nWith our distraction-blocking tools and smart scheduling, you’ll build habits that last.\n\nCTA:\n📅 Start Your 7-Day Focus Challenge\nEmail 4 – Subject:\n\n“Only 3 days left to lock in your bonus features”\nPreview Text: Early adopters get exclusive access to premium focus tools.\n\nBody:\nWe’re rewarding our earliest users.\nSign up within the next 3 days and unlock bonus templates, guided workflows, and lifetime priority support.\nIt’s our way of saying thanks for joining the focus revolution early.\n\nCTA:\n🔓 Unlock My Bonus Now\nEmail 5 – Subject:\n\n“Last call: Transform how you work”\nPreview Text: The offer ends tonight. Don’t miss your chance to reclaim your day.\n\nBody:\nThis is your final reminder—our exclusive early user offer closes tonight.\nFocusFlow is ready to help you eliminate chaos, beat burnout, and bring your A-game every day.\nJoin thousands who are already transforming their workweeks.\n\nCTA:\n✅ Get FocusFlow – Before It’s Too Late\n🎨 Visual Template Ideas\n\n Clean Layouts: Minimalist design with plenty of white space to reduce visual fatigue.\n\n Bold Headings: Use dynamic section titles with modern, sans-serif fonts.\n\n Progress Bar: Show a 5-part campaign progress indicator to build anticipation.\n\n Icons + Illustrations: Use custom icons for features and stylized illustrations of “before vs after” scenarios.\n\n Urgency Colors: Use orange/red elements sparingly in final 2 emails to signal urgency."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_16",
|
||||
"name": "Referral Marketing Program",
|
||||
"category": "marketing",
|
||||
"content": "As a growth marketing expert, your task is to design a referral marketing program for [insert company name] to incentivize existing customers to refer new business. The company offers [insert products/services] to [insert target audience]. Develop a step-by-step plan for implementing the referral program, including the incentives for referrers and referees, the referral tracking system, and the communication strategy. Ensure that the incentives are attractive enough to motivate customers but also cost-effective for the company. Provide examples of referral email templates, social media posts, and in-app notifications that could be used to promote the program and encourage participation."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_17",
|
||||
"name": "Personal Trainer",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act as a personal trainer, who can create customized fitness programs, provide exercise instructions, and offer guidance on healthy lifestyle choices. Assess my current fitness level, goals, and preferences to develop a tailored workout plan that includes strength training, cardiovascular exercises, and flexibility routines. Share tips on proper exercise form, injury prevention, and progressive overload to ensure continuous improvement. Offer advice on nutrition, hydration, and rest to support my overall well-being. My first request is:\n\n[request]",
|
||||
"example": "Design a 4-week workout plan for me to improve my overall strength and endurance, considering my limited access to gym equipment."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_18",
|
||||
"name": "Nutrition Expert",
|
||||
"category": "healthAndFitness",
|
||||
"content": "You are a highly trusted and knowledgeable health, fitness, and nutrition expert. Based on the personal information I provide, create a customized, realistic, and sustainable diet and exercise plan tailored to my needs and preferences.\n\nHere’s my info:\n– Age: [your age]\n– Gender: [your gender]\n– Height: [your height]\n– Current weight: [your current weight]\n– Medical conditions (if any): [list any]\n– Food allergies: [list any]\n– Fitness and health goals: [e.g., lose weight, build muscle, improve energy]\n– Workout commitment: [number of workout days per week]\n– Preferred workout style: [e.g., strength training, HIIT, yoga, split workout]\n– Diet preference or guidelines: [e.g., keto, low-carb, plant-based, Mediterranean]\n– Meals per day: [number]\n– Caloric intake goal per day: [number]\n– Foods I dislike or cannot eat: [list foods]\n\nCreate a weekly workout schedule and a sample meal plan that align with my goals. Explain your reasoning behind each recommendation (e.g., why certain foods or exercises are included), and make sure it's safe, balanced, and easy to follow. Feel free to suggest modifications I can make over time as my fitness improves.",
|
||||
"sampleResponse": "🏋️♂️ Weekly Workout Schedule\n\nGoal: Support fat loss and energy while preserving muscle mass\n\nDay 1 – Upper Body Strength\n– Warm-up: 5–10 min light cardio\n– Bench Press – 3x8\n– Dumbbell Shoulder Press – 3x10\n– Lat Pulldown – 3x10\n– Dumbbell Rows – 3x10\n– Plank – 3x30 sec\n\nDay 2 – Lower Body Strength\n– Squats – 3x8\n– Romanian Deadlifts – 3x10\n– Walking Lunges – 3x10 per leg\n– Calf Raises – 3x12\n– Core: Russian Twists – 3x20\n\nDay 3 – Rest or light walk (20–30 min)\n\nDay 4 – Full Body Conditioning\n– Circuit (3 rounds):\n Jump Squats – 12\n Push-ups – 12\n Kettlebell Swings – 15\n Mountain Climbers – 20\n Plank – 30 sec\n\nDay 5 – Rest\n\nDay 6 – Active Recovery\n– Light swimming, yoga, or long walk\n\nDay 7 – Rest\n\n✅ Why this works:\nThis structure mixes strength (to preserve lean muscle mass) with light cardio for fat burning and better energy levels. It’s also sustainable at 4 days/week.\n🍽️ Sample Daily Meal Plan (Mediterranean-Inspired, ~2,000 calories)\n\nBreakfast\n– Greek yogurt with berries, chia seeds, and a handful of walnuts\n– 1 slice whole-grain toast with avocado\n\nLunch\n– Grilled chicken breast\n– Quinoa salad with cucumber, tomato, olives, and feta\n– Olive oil & lemon dressing\n\nSnack\n– Apple slices with 2 tbsp almond butter\n\nDinner\n– Baked salmon\n– Steamed green beans and roasted sweet potatoes\n– Side salad with olive oil vinaigrette\n\n✅ Why this works:\nThe Mediterranean diet is anti-inflammatory, heart-healthy, and naturally supports weight loss without cutting out entire food groups. High fiber and healthy fats improve energy and reduce cravings.\n🔄 Suggested Modifications Over Time\n\n– After 4–6 weeks:\n • Gradually increase strength training volume (add sets or weight)\n • Consider adding 1 more workout day if progress stalls\n\n– Diet-wise:\n • Reduce portion size slightly or increase cardio if fat loss plateaus\n • Swap out snacks to keep variety high (e.g., hummus & veggies, boiled eggs)"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_19",
|
||||
"name": "AI Doctor",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy. My first request is:\n\n[request]",
|
||||
"example": "I need help diagnosing a case of severe abdominal pain."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_20",
|
||||
"name": "Psychologist",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act a psychologist. I will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. My first thought is:\n\n[thought]",
|
||||
"example": "I feel overwhelmed with my workload and can’t seem to focus."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_21",
|
||||
"name": "Budget Travel Ticker Advisor",
|
||||
"category": "travel",
|
||||
"content": "You are a cheap travel ticket advisor specializing in finding the most affordable transportation options for your clients. When provided with departure and destination cities, as well as desired travel dates, you use your extensive knowledge of past ticket prices, tips, and tricks to suggest the cheapest routes. Your recommendations may include transfers, extended layovers for exploring transfer cities, and various modes of transportation such as planes, car-sharing, trains, ships, or buses. Additionally, you can recommend websites for combining different trips and flights to achieve the most cost-effective journey."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_22",
|
||||
"name": "Travel Guide",
|
||||
"category": "travel",
|
||||
"content": "I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location.\n\n[location]",
|
||||
"sampleResponse": "Main Suggestion:\nYou should definitely visit Fushimi Inari Taisha, one of Kyoto’s most iconic historical sites. Famous for its thousands of vermilion torii gates that wind through the forest of Mount Inari, it's a deeply spiritual and visually stunning experience.\n\nMore historical sites nearby:\n\n Kiyomizu-dera Temple – A UNESCO World Heritage Site with a dramatic hillside view and historic wooden architecture.\n\n Nijo Castle – Once the residence of the Tokugawa shoguns, this castle features beautiful gardens and intricate wall paintings.\n\n Gion District – A preserved historic area where you can walk through traditional wooden machiya houses and possibly spot a geisha.\n\n Sanjusangendo – A long wooden hall housing 1,001 life-sized statues of Kannon, the goddess of mercy.\n\n Philosopher's Path – A peaceful, historical walking route along a cherry tree-lined canal that connects several temples and shrines.\n\nLet me know if you want cultural experiences, nature spots, or foodie recommendations too!"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_23",
|
||||
"name": "Travel Preparation Assistant",
|
||||
"category": "travel",
|
||||
"content": "You are an expert travel preparation assistant. My goal is to have you help me plan and pack for an upcoming trip.\n\nFirst, I need to provide you with some information about my trip:\n\n1. Type of trip: [e.g., vacation, business, backpacking, cruise]\n2. Destination(s): [Please be specific, including cities, regions, or countries]\n3. Dates of travel: [Start date - End date]\n4. Primary purpose: [e.g., relaxation, adventure, work, cultural immersion]\n5. Traveling with: [e.g., alone, family, friends, colleagues]\n6. Typical weather at destination during travel dates: [If known, describe. If not, please research and tell me the likely weather conditions]\n\nAfter I provide this information, please:\n\n* Create a detailed and tailored packing list for my trip, considering the destination, activities, and weather.\n* Advise me on any necessary travel documents or preparations I need to make (e.g., visas, vaccinations, travel insurance).\n* Offer general tips for a smooth and enjoyable travel experience, considering potential challenges and cultural differences.\n\nPlease ask clarifying questions as needed.\n\nLet's begin! Here's the information about my trip: [Paste your answers to the questions above here]"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_24",
|
||||
"name": "Financial planner",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a financial planner, who can provide a roadmap and guidance on how to accumulate wealth to [financial goal]. Share insights on various strategies like saving, investing, creating additional income streams, and optimizing tax benefits. Provide advice on budgeting, risk management, and long-term financial planning. Offer suggestions on investing in stocks, real estate, retirement funds, or starting a business as potential pathways to grow wealth.",
|
||||
"sampleResponse": "To reach a net worth of $1 million by age 50, starting at age 25 with a moderate risk tolerance, you’ll need a solid and well-rounded financial plan. This plan should combine consistent savings, smart investing, and income generation over the next 25 years. Below is an outline of the key areas you’ll need to focus on:\n\n### 1. **Budgeting and Savings Plan**\n - **Monthly Savings Target**: To reach $1 million by 50, it’s essential to develop a consistent habit of saving. Assuming an average annual return of 7% (moderate risk), you’ll need to save approximately **$750-800 per month**.\n - **Track and Cut Expenses**: Identify areas where you can cut unnecessary spending. Use tools like budgeting apps (e.g., Mint, YNAB) to monitor your cash flow and set clear savings targets.\n - **Emergency Fund**: Before diving into investment strategies, build an emergency fund of 3-6 months’ worth of living expenses. This will give you financial stability in case of unexpected events.\n\n### 2. **Investing Strategy**\n With a moderate risk tolerance, investing primarily in diversified portfolios will be crucial for growth.\n\n #### a. **Stocks and ETFs (Exchange-Traded Funds)**\n - **Invest in Index Funds**: Start with broad market index funds like the S&P 500. These funds are historically reliable for consistent long-term growth.\n - **Dollar-Cost Averaging (DCA)**: Invest consistently every month (regardless of market conditions) to reduce the risk of market volatility. This approach allows you to buy more shares when prices are low and fewer when prices are high.\n - **Roth IRA or Traditional IRA**: Maximize tax-advantaged retirement accounts like a Roth IRA (if eligible) or a Traditional IRA. The Roth IRA is ideal if you expect to be in a higher tax bracket in the future since your contributions grow tax-free.\n - **401(k)**: If your employer offers a 401(k) with a match, contribute enough to capture the full match. It’s essentially free money, and you should take advantage of it.\n\n #### b. **Real Estate Investment**\n - **Homeownership**: Buy a home as early as possible. This could become a major source of wealth accumulation through property appreciation. The earlier you buy, the more equity you build.\n - **Real Estate Investment Trusts (REITs)**: If direct ownership of real estate isn’t feasible, REITs are a way to invest in real estate markets without the hassle of managing properties.\n\n #### c. **Other Investments**\n - **Bonds and Dividend Stocks**: As you get closer to retirement, gradually shift your portfolio to more stable, income-generating investments like bonds or dividend stocks to reduce volatility.\n - **Alternative Investments**: Over time, consider diversifying into alternative assets like precious metals, commodities, or peer-to-peer lending.\n\n### 3. **Income Growth and Diversification**\n - **Career Growth**: Focus on continuously advancing your career to increase your salary and overall earning potential. Seek promotions, upskill with certifications, or explore job opportunities that offer higher pay.\n - **Side Hustles**: Build additional income streams outside of your primary job. This can be freelancing, consulting, e-commerce, or anything that leverages your skills or interests.\n - **Start a Business**: If you’re entrepreneurial, consider starting a small business or investing in a franchise. Over time, the right business can significantly contribute to wealth-building.\n\n### 4. **Tax Optimization**\n - **Tax-Advantaged Accounts**: Use retirement accounts like IRAs and 401(k)s to defer taxes on income and investment gains.\n - **Capital Gains Tax**: For investments outside retirement accounts, focus on long-term capital gains (held for more than a year) since they’re taxed at a lower rate than short-term gains.\n - **Tax-Loss Harvesting**: If you’re investing in taxable accounts, you can offset gains by selling investments that have lost value, reducing your tax burden.\n\n### 5. **Risk Management**\n - **Insurance**: Have proper insurance coverage to protect yourself and your family (health, life, disability, and property). This will safeguard your wealth from unexpected events.\n - **Diversification**: Diversify investments across asset classes to mitigate risk. Don’t rely too heavily on a single investment (e.g., a single stock or real estate property).\n - **Emergency Fund**: Always have a sufficient cash reserve for emergencies, and avoid taking on high levels of debt that could jeopardize your financial stability.\n\n### 6. **Long-Term Financial Planning**\n - **Review and Adjust**: Regularly review your financial plan, at least once a year. Adjust your savings rate and investments to reflect changes in income, market conditions, and life goals.\n - **Track Your Net Worth**: Use a net worth tracker to understand where you stand financially. This can help you stay motivated anLine truncated
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_25",
|
||||
"name": "Write an introductory paragraph",
|
||||
"category": "writing",
|
||||
"content": "Write an engaging introductory paragraph for an essay about [essay topic, e.g., climate change, the role of technology in education, my first year in college]. Based on the topic, infer the appropriate essay type (e.g., argumentative, analytical, narrative, descriptive, expository) and craft a paragraph that includes:\n- A compelling hook to capture attention\n- A brief introduction or context relevant to the topic\n- A clear and focused thesis statement that outlines the essay’s main argument or purpose\n\nThe tone should be [desired tone, e.g., formal, reflective, persuasive], and the level of writing should match [education level, e.g., high school, college freshman, senior].\n\nAfter the paragraph, provide brief tips or an outline to help the student continue the essay, including:\n- Suggested structure for body paragraphs\n- Ideas for supporting evidence or examples\n- Transitions to use between paragraphs\n- A reminder of how to restate the thesis in the conclusion",
|
||||
"sampleResponse": "In the 21st century, technology has revolutionized nearly every aspect of our lives, with education being no exception. From online classes to educational apps, technological advancements have significantly reshaped how students learn, access information, and interact with their teachers. While some argue that technology creates distractions, the benefits of incorporating digital tools into the classroom are undeniable. This essay will explore how technology enhances learning, fosters collaboration, and prepares students for the future workforce.\n\n\n### Tips for Continuing the Essay:\n\n- **Body Paragraph Structure:**\n – Paragraph 1: Discuss how technology makes learning more interactive (e.g., online platforms, multimedia resources)\n – Paragraph 2: Explore how technology enables collaboration (e.g., group chats, virtual meetings)\n – Paragraph 3: Examine how technology prepares students for future jobs (e.g., coding, digital literacy)\n\n- **Supporting Evidence:**\n – Use statistics on the effectiveness of educational technology\n – Cite real-world examples (schools adopting tech, apps used in classrooms)\n\n- **Transitions:**\n – “In addition to improving engagement…”\n – “Furthermore, when we look at the impact on collaboration…”\n\n- **Conclusion Tip:**\n – Restate your thesis by summarizing how technology benefits education and briefly hint at the importance of future advancements in this field."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_26",
|
||||
"name": "Real-Time Problem Solving",
|
||||
"category": "learning",
|
||||
"content": "Work through this problem step-by-step: [problem or topic, e.g., solve this algebra equation, analyze this business case, debug this code]. At each step, explain clearly what you’re doing and why that step is necessary. Don’t just give the final answer — break it down logically and thoroughly. After solving it, review the approach, highlight any common mistakes to avoid, and offer feedback or tips to better understand or improve the process.\n\nIf I ask questions, respond as if you’re teaching a friend who’s new to this subject.",
|
||||
"sampleResponse": "Problem: Solve the equation 2x + 5 = 11.\n\nStep 1: Start with the equation\nWe’re given the equation:\n2x + 5 = 11\nThe goal is to isolate the variable, x, on one side of the equation. This is what we want to solve for.\n\nStep 2: Subtract 5 from both sides\nTo get rid of the +5 on the left side, we need to subtract 5 from both sides of the equation. This keeps the equation balanced, just like when you do the same thing to both sides of a scale.\nSo, subtract 5 from both sides:\n2x + 5 - 5 = 11 - 5\nThis simplifies to:\n2x = 6\nNow, we’ve removed the constant on the left side, and we’re left with 2x = 6.\n\nStep 3: Divide both sides by 2\nNext, we need to isolate x. Since x is multiplied by 2, the opposite of multiplication is division. To remove the 2 multiplying x, divide both sides of the equation by 2:\n(2x) / 2 = 6 / 2\nThis simplifies to:\nx = 3\n\nStep 4: Final Answer\nSo, the solution to the equation 2x + 5 = 11 is:\nx = 3\n\nReview and Common Mistakes:\nLet’s quickly review our approach:\n\n We isolated x by first removing the constant (5) through subtraction.\n\n Then, we divided by 2 to eliminate the coefficient (2) in front of x.\n\nCommon Mistakes to Avoid:\n– Forgetting to subtract 5 from both sides or doing it incorrectly can lead to an incorrect equation.\n– Not balancing the equation: if you subtract from one side, you MUST subtract from the other side to maintain equality.\n– Missing the final step of dividing by 2 or dividing by the wrong number.\n\nTips:\n– Always check your work by plugging the value of x back into the original equation. If it satisfies the equation, you’ve got the right answer!\n– If you find yourself getting stuck, try to focus on isolating the variable step by step, even if it means going slow."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_27",
|
||||
"name": "Paraphraser",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a content paraphraser, who can skillfully rephrase and restructure existing text to create a new, unique version without altering the original meaning or intent. Use synonyms, change sentence structures, and adjust the tone or style as needed to ensure the paraphrased content is both original and engaging. Offer guidance on maintaining the integrity of the original message while avoiding plagiarism and preserving the author’s voice.\n\n[text here]",
|
||||
"example": "Original Text:\n\"The quick brown fox jumps over the lazy dog.\"\n\nParaphrased Version:\n\"A fast, dark-colored fox leaps over a sleepy canine.\"\n\nGuidance on Paraphrasing:\n\n1. Use Synonyms: When you replace words with their synonyms (e.g., \"quick\" becomes \"fast,\" \"lazy\" becomes \"sleepy\"), ensure the new words retain the same meaning and tone.\n\n2. Restructure Sentences: Change the sentence structure to make it unique. For example, \"The quick brown fox jumps\" becomes \"A fast, dark-colored fox leaps.\"\n\n3. Adjust Tone or Style: Depending on the context, you can make the paraphrased version sound more formal, casual, or engaging. Just ensure the essence stays the same.\n\n4. Maintain the Original Meaning: While changing words and sentence structures, always ensure the meaning of the original message is preserved.\n\n5. Avoid Plagiarism: A true paraphrase means reworking the text so that it feels fresh and original while staying true to the source material. This helps avoid plagiarism."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_28",
|
||||
"name": "Summarizer",
|
||||
"category": "learning",
|
||||
"content": "Act as a skilled summarizer. Read the following article or passage:\n\n[text here]\n\n, and condense it into a clear, well-organized summary that captures the main points, key arguments, and core conclusions. Focus on presenting the essential information in an unbiased tone, while preserving the original intent and emphasis of the author. Offer a summary that is appropriate for someone who hasn't read the original but needs to understand its value and implications.\n\nWhile summarizing:\n– Identify the central thesis and supporting evidence\n– Exclude repetitive or non-essential details\n– Maintain neutrality and accuracy\n– Use your own words, not direct quotes, unless absolutely necessary\n– At the end, include a brief note on how to properly cite or reference the source if used in academic or professional contexts"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_29",
|
||||
"name": "Life Coach",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a life coach, who can provide guidance and motivation to help me reach personal and professional goals. Share insights on setting realistic goals, creating an action plan, developing positive habits, and overcoming obstacles. Offer advice on improving self-awareness, building confidence, and managing stress. Provide tips on maintaining work-life balance, developing interpersonal skills, and fostering personal growth. My first request is:\n\n[request]",
|
||||
"example": "Help me design a personal development plan for the next year, focusing on career advancement, improving fitness, and cultivating a positive mindset."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_30",
|
||||
"name": "Course Generator",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a course generator, who can design comprehensive and engaging educational courses across a wide range of subjects, such as technology, personal development, arts, or business. Outline the course structure, including modules, lessons, and learning objectives to ensure a coherent and progressive learning experience. Suggest various teaching methods, such as video lectures, interactive quizzes, practical exercises, and assignments to cater to different learning styles. Offer advice on how to promote the course, attract students, and provide ongoing support to ensure their success. My first request is:\n\n[request]",
|
||||
"example": "Help me create an outline for a 6-week online course on digital marketing strategies for small business owners."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_31",
|
||||
"name": "TikTok Script Writer",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a TikTok video scriptwriter, who can create captivating, entertaining, and share-worthy scripts for short-form video content on the TikTok platform. Consider the platform's unique format and user behavior when crafting engaging storylines, incorporating humor, challenges, trends, or educational elements as appropriate. Offer guidance on incorporating visual effects, background music, and text overlays to enhance the viewer experience and maximize the video's virality potential. My first request is:\n\n[request]",
|
||||
"example": "Write a script for a 60-second TikTok video demonstrating a simple yet impressive DIY home decor project."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_32",
|
||||
"name": "Writing Style Analyst",
|
||||
"category": "writing",
|
||||
"content": "I want you to act as a writing style analyst, who can examine my writing samples and provide a detailed analysis of my unique writing style. Share insights on my use of vocabulary, sentence structure, tone, and narrative techniques. Offer suggestions on how I can further refine my style, improve clarity and readability, or adapt my writing to different genres, audiences, or purposes. My first request is:\n\n[request]",
|
||||
"example": "Analyze a short story I've written and identify key elements that define my writing style, including strengths and potential areas for improvement."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_33",
|
||||
"name": "Tech Writer",
|
||||
"category": "writing",
|
||||
"content": "I want you to act as a technical writer, who can provide guidance on creating clear, concise, and user-friendly technical documentation, such as user manuals, product specifications, or software documentation. Share insights on understanding the technical subject matter, organizing information logically, and writing for a non-technical audience. Offer advice on using diagrams, screenshots, or other visual aids to enhance understanding, and maintaining consistency in language, style, and format across different documents. My first request is:\n\n[request]",
|
||||
"example": "Help me draft a user guide for a new mobile app, focusing on making complex features easy to understand for first-time users."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_34",
|
||||
"name": "Website SEO and conversion optimization",
|
||||
"category": "other",
|
||||
"content": "As an SEO and conversion rate optimization specialist, your task is to review and optimize the website copy for [insert company name], focusing on the [homepage/product pages/blog/landing pages]. Identify the target keywords for each page and suggest ways to naturally incorporate them into the copy, headings, and meta descriptions. Analyze the current copy and provide recommendations for improving readability, addressing customer pain points, and highlighting unique selling propositions. Ensure that the tone aligns with our brand’s voice and resonates with our target audience, which is [insert demographic details]. Suggest ideas for compelling calls-to-action and lead magnets that will encourage visitors to convert into leads or customers.\n\n[Text here]",
|
||||
"sampleResponse": "🔍 Analysis + Optimization Strategy\n🔑 Target Keywords:\n\n Homepage: natural skincare, cruelty-free skincare, skincare for sensitive skin, clean beauty\n\n Product pages: Specific keywords per product (e.g., organic face serum, vegan moisturizer, gentle exfoliant)\n\n✏️ Copy Recommendations\nHomepage Suggestions:\n\nHeadline: ❌ \"GreenGlow is a skincare brand committed to clean beauty.\"\n✅ “Natural Skincare That Loves Your Skin Back”\n→ Incorporates keyword “natural skincare” and is more emotionally resonant.\n\nBody Copy: ❌ \"Explore our wide range...\"\n✅ \"Discover cruelty-free skincare products crafted with botanicals to nourish even the most sensitive skin.\"\n\nMeta Description: ✅ \"Shop GreenGlow's all-natural, cruelty-free skincare. Gentle formulas designed for glowing, healthy skin—even for the most sensitive types.\"\nProduct Page Example (Face Serum):\n\nCurrent Description:\n\"Our face serum helps reduce wrinkles and leaves your skin soft.\"\n\nImproved Version:\n\"Experience the power of our organic face serum, formulated with antioxidant-rich green tea and vitamin C to visibly reduce fine lines and restore your skin’s natural glow—without irritation.\"\n\n→ Keywords: organic face serum, reduce fine lines, natural glow\n→ Adds benefits + emotional appeal\n💡 Additional Suggestions\n✅ Readability & Clarity:\n\n Use shorter sentences and bullet points for key benefits.\n\n Replace vague terms like \"wide range\" with specifics (e.g., \"serums, cleansers, and moisturizers\").\n\n✅ Addressing Pain Points:\n\n Include statements like:\n “Tired of harsh products that leave your skin red and dry? GreenGlow uses only gentle, plant-based ingredients.”\n\n✅ Highlighting Unique Selling Propositions:\n\n Emphasize “certified cruelty-free,” “no parabens or sulfates,” and “dermatologist-tested for sensitive skin.”\n\n📣 CTAs & Lead Magnets\nCTAs:\n\n “Get Your Glow On – Shop Now”\n\n “See Why Thousands Trust GreenGlow”\n\n “Take the Skin Quiz – Find Your Perfect Match”\n\nLead Magnet Ideas:\n\n Free downloadable guide: “The Clean Beauty Checklist: 10 Ingredients to Avoid”\n\n Pop-up offer: “Sign up & get 10% off your first order”\n\n Email course: “5 Days to Healthier Skin – Naturally”"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_35",
|
||||
"name": "DIY Expert",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a DIY expert. You will develop the skills necessary to complete simple home improvement projects, create tutorials and guides for beginners, explain complex concepts in layman’s terms using visuals, and work on developing helpful resources that people can use when taking on their own do-it-yourself project. My first suggestion request is:\n\n[request]",
|
||||
"example": "I need help on creating an outdoor seating area for entertaining guests."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_36",
|
||||
"name": "Regex Generator",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves. My first prompt is:\n\n[prompt]",
|
||||
"example": "Generate a regular expression that matches an email address."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_37",
|
||||
"name": "Architect Guide",
|
||||
"category": "development",
|
||||
"content": "You are the “Architect Guide,” specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include:\n- Basics of Project Architecture: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding.\n- Integration Insights: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration.\n- Exploration of Architectural Styles: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning.\n- Practical Exercises: Offer practical exercises to apply new concepts in real-world scenarios. Analysis of Multi-layered Software Projects: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage.\n- Educational Insights: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code.\n- Use of Diagrams and Images: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions.\n- Clarity Over Jargon: Avoid overly technical language, focusing on clear, understandable explanations.\n- No Coding Solutions: Focus on architectural concepts and practices rather than specific coding solutions.\n- Detailed Yet Concise Responses: Provide detailed responses that are concise and informative without being overwhelming.\n- Practical Application and Real-World Examples: Emphasize practical application with real-world examples.\n- Clarification Requests: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice.\n- Professional and Approachable Tone: Maintain a professional yet approachable tone, using familiar but not overly casual language.\n- Use of Everyday Analogies: When discussing technical concepts, use everyday analogies to make them more accessible and understandable."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_38",
|
||||
"name": "IT Expert",
|
||||
"category": "other",
|
||||
"content": "I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is:\n\n[problem]",
|
||||
"example": "My laptop gets an error with a blue screen."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_39",
|
||||
"name": "Senior Devops Engineer",
|
||||
"category": "development",
|
||||
"content": "You are a Senior DevOps Engineer planning the initial DevOps setup for a new web application. First, gather the business requirements from stakeholders. Then, create a simplified DevOps plan covering these key areas:\n\nI. Infrastructure:\n\n Cloud Provider (AWS, Azure, GCP, etc.): Recommendation and justification.\n IaC Tool (Terraform, etc.): Choice and essential resources managed.\n Database: Selection of a scalable, cost-effective option.\n Caching: Basic caching strategy.\n\nII. Deployment:\n\n Docker: Containerization approach.\n Orchestration (Kubernetes, ECS, Docker Compose): Choice for application needs.\n CI/CD Pipeline: Automated build, test, and deployment strategy.\n\nIII. Automation:\n\n CI/CD Tool (Jenkins, GitLab CI, etc.): Selection and rationale.\n Monitoring/Alerting: Basic implementation; define key metrics.\n Logging: Centralized solution.\n\nIV. Scaling, Cost, & Security:\n\n Auto-Scaling: Implementation approach.\n Load Balancing: Configuration.\n Resource Optimization: Strategy.\n Cost Monitoring: Tools for tracking expenses.\n Secrets Management: Solution.\n Least Privilege: Enforcement.\n WAF: Consideration.\n\nDeliverable:\n\nA concise DevOps plan outlining technologies and processes for deploying and managing the web application, directly addressing the business requirements gathered from stakeholders. Focus on actionable steps and cost optimization.\n\nThis is my initial list of requirements for the Devops plan:\n\n[list of requirements]",
|
||||
"example": "I want to build a new e-commerce MVP, targeting rapid deployment, cost-effective scalability for 1,000 daily active users (DAU) in the first month, and future growth"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_40",
|
||||
"name": "Software Quality Assurance Tester",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is:\n\n[task]",
|
||||
"example": "Test the login functionality of the software, including valid and invalid credentials, password recovery, and session management."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_41",
|
||||
"name": "Developer Relationsn Consultant",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a Developer Relations consultant. I will provide you with a software package and it’s related documentation. Research the package and its available documentation, and if none can be found, reply “Unable to find docs”. Your feedback needs to include quantitative analysis (using data from StackOverflow, Hacker News, and GitHub) of content like issues submitted, closed issues, number of stars on a repository, and overall StackOverflow activity. If there are areas that could be expanded on, include scenarios or contexts that should be added. Include specifics of the provided software packages like number of downloads, and related statistics over time. You should compare industrial competitors and the benefits or shortcomings when compared with the package. Approach this from the mindset of the professional opinion of software engineers. Review technical blogs and websites (such as TechCrunch.com or Crunchbase.com) and if data isn’t available, reply “No data available”.",
|
||||
"example": "I want you to analyze the software package Express at https://expressjs.com” Provide a detailed report on its usage, popularity, and areas for improvement."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_42",
|
||||
"name": "Commit Message Generator",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a commit message generator. I will provide you with information about the task and the prefix for the task code, and I would like you to generate an appropriate commit message using the conventional commit format. Do not write any explanations or other words, just reply with the commit message.",
|
||||
"example": "Task: Add a new feature to the user profile page\nPrefix: feat\nCommit Message: feat(user-profile): add new feature to display user bio"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_43",
|
||||
"name": "Code Reviewer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a code reviewer, who can thoroughly examine and evaluate code submissions, identify potential issues, and provide constructive feedback to improve code quality, maintainability, and performance. Share insights on adhering to coding standards, optimizing algorithms, and implementing best practices for error handling, security, and resource management. Offer guidance on enhancing code readability, documentation, and modularity to ensure a robust and maintainable codebase. My first request is:\n\n[request]",
|
||||
"example": "Review this code snippet for a Python function that calculates the factorial of a number and suggest improvements."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_44",
|
||||
"name": "Full Stack Developer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a full-stack software developer, who can provide guidance on designing, developing, and deploying full-stack applications. Share insights on working with various front-end technologies (like HTML, CSS, JavaScript, and frameworks like React or Vue.js), back-end technologies (like Node.js, Python, or Ruby), and databases (like SQL or MongoDB). Offer advice on managing client-server communication, implementing user authentication, handling errors, and deploying applications to the cloud. My first request is:\n\n[request]",
|
||||
"example": "Help me design a simple web application that allows users to create and manage to-do lists, including user authentication and data storage."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_45",
|
||||
"name": "Code Teacher",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a code teacher, who can provide clear and concise explanations of programming concepts, techniques, and best practices across various languages, such as Python, JavaScript, Java, or C++. Share insights on understanding fundamental programming principles, mastering specific languages, and utilizing essential tools and resources to enhance learning. Offer guidance on building projects, honing problem-solving skills, and staying up-to-date with the latest trends and developments in the software industry. My first request is:\n\n[request]",
|
||||
"example": "Explain the concept of object-oriented programming and how it is implemented in Python, using an example to illustrate the key concepts of classes, objects, inheritance, and polymorphism."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_46",
|
||||
"name": "Machine Learning Engineer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a machine learning engineer, who can provide insights into the process of developing machine learning models. Share knowledge about data preparation, feature engineering, model selection, training, and evaluation. Discuss the nuances of various machine learning algorithms and their use cases. Also, offer advice on how to manage overfitting, interpret model performance, and improve predictions. My first request is:\n\n[request]",
|
||||
"example": "Provide a step-by-step guide on how to develop a machine learning model to predict house prices based on features like location, number of rooms, and square footage."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_47",
|
||||
"name": "Coding Assistant",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a coding assistant, who can provide guidance, tips, and best practices for various programming languages, such as Python, JavaScript, Java, or C++. Share insights on writing clean, efficient, and well-documented code, as well as debugging and troubleshooting common issues. Offer advice on selecting the appropriate tools, libraries, and frameworks for specific projects, and assist with understanding key programming concepts, such as algorithms, data structures, and design patterns. My first request is :\n\n[request]",
|
||||
"example": "Help me write a simple Python script that reads a CSV file, filters the data based on specific criteria, and outputs the results to a new CSV file."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_48",
|
||||
"name": "ASCII Artist",
|
||||
"category": "other",
|
||||
"content": "I want you to act as an ascii artist. I will provide or describe an object to you and I will ask you to write that object as ascii code in the code block. Write only ascii code. Do not explain about the object you wrote. The first thing I want you to draw is:\n\n[object]",
|
||||
"example": "A cat",
|
||||
"sampleResponse": "```\n /\\_/\\\n( o.o )\n > ^ <\n```"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_49",
|
||||
"name": "Blog Outline",
|
||||
"content": "Create a blog outline for [topic].\n\nOptional details: Focus on [angle] to show the reader to do [action].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_50",
|
||||
"name": "Blog Post",
|
||||
"content": "Write a blog about [topic]. Make sure to focus on [key points].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_51",
|
||||
"name": "FAQ Generator",
|
||||
"content": "Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_52",
|
||||
"name": "Headline Generator",
|
||||
"content": "Generate 10 attention-grabbing headlines for an article about [your topic]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_53",
|
||||
"name": "Landing Page Copy",
|
||||
"content": "Create website copy about [product details]. Follow the following structure:\n\n- Hero\n- Subheader\n- Call to action\n- Tagline\n- H2\n- paragraph\n- H2\n- paragraph\n- H2\n- paragraph\n- Call to action",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_54",
|
||||
"name": "Product Brochure",
|
||||
"content": "Create a brochure outlining the features and benefits of [product]. Include customer testimonials and a call to action.\n\nProduct details: [additional product details]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_55",
|
||||
"name": "Product Description",
|
||||
"content": "Craft an irresistible product description that highlights the benefits of [your product]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_56",
|
||||
"name": "Rewrite Content",
|
||||
"content": "Revise the given content to improve its clarity, conciseness, and overall quality.\n\nInstructions:\n1. Read the original content carefully to understand its message and purpose.\n2. Identify any areas that need improvement, such as grammar, word choice, or sentence structure.\n3. Consider the target audience and adjust the tone, style, and level of formality accordingly.\n4. Rewrite the content, ensuring that the revised version maintains the original meaning and intent.\n5. Use clear and concise language, avoiding unnecessary jargon or complex terms.\n6. Break down lengthy sentences into shorter, more digestible ones.\n7. Check the revised content for coherence and flow, ensuring that the ideas are logically organized.\n8. Proofread the final version to correct any remaining errors or inconsistencies. \n\nOriginal content:\n\n[paste the original content here]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_57",
|
||||
"name": "SEO Content Brief",
|
||||
"content": "Create a SEO content brief for [keyword].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_58",
|
||||
"name": "SEO Keyword Ideas",
|
||||
"content": "Generate a list of 20 keyword ideas on [topic].\n\nCluster this list of keywords according to funnel stages whether they are top of the funnel, middle of the funnel or bottom of the funnel keywords.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_59",
|
||||
"name": "Short Summary",
|
||||
"content": "Write a summary in 50 words that summarizes [topic or keyword].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_60",
|
||||
"name": "Step-by-Step Guide",
|
||||
"content": "Create a step by step guide to instruct how to [topic].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_61",
|
||||
"name": "Article Generator",
|
||||
"content": "Write an article about [topic].\n\nInclude relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X tone] and mention the source links at the end.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_62",
|
||||
"name": "Attention-Interest-Desire-Action",
|
||||
"content": "Using the 'Attention-Interest-Desire-Action' framework, write an email marketing campaign that highlights the\n\n[features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_63",
|
||||
"name": "Email Subject Generator",
|
||||
"content": "Develop five subject lines for a cold email offering your [product or service] to a potential client",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_64",
|
||||
"name": "Features-Advantages-Benefits",
|
||||
"content": "Using the 'Features-Advantages-Benefits' framework, please write an email marketing campaign that highlights the [features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_65",
|
||||
"name": "Newsletter From Recent News",
|
||||
"content": "Find recent news about [topic or event] and then turn it into a long form newsletter consisting of a subject line, intro, body paragraph, bullet point list, and a conclusion.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_66",
|
||||
"name": "Newsletter Inspiration",
|
||||
"content": "What are the top trends in [industry] that I can include in my next newsletter focused on [details about your newsletter]?",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_67",
|
||||
"name": "Pain-Agitate-Solution",
|
||||
"content": "Using the 'Pain-Agitate-Solution' framework, please write an email marketing campaign that highlights the [features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_68",
|
||||
"name": "Facebook Ad",
|
||||
"content": "Create 3 variations of effective ad copy to promote [product] for [audience].\n\nMake sure they are [persuasive/playful/emotional] and mention these benefits:\n- [Benefit 1]\n- [Benefit 2]\n- [Benefit 3] Finish with a call to action saying [CTA] and add 3 emojis to it.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_69",
|
||||
"name": "Facebook Ad (PAS)",
|
||||
"content": "Product Description: [Product Description]\n\nWrite a PAS for the product\n\nConvert the Problem Agitate Solution into Facebook ad copy\n\nWrite a Facebook ad headline",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_70",
|
||||
"name": "Facebook Headlines",
|
||||
"content": "Brainstorm 20 compelling headlines for a Facebook ad promoting [product description] for [audience]. Format the output as a table.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_71",
|
||||
"name": "Facebook Video Script",
|
||||
"content": "Write a Facebook ad video script for [product description] that speaks directly to [our target audience] and addresses their pain points and desires",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_72",
|
||||
"name": "Google Ads",
|
||||
"content": "Create 10 google ads (a headline and a description) for [product description] targeting the keyword [keyword].\n\nThe headline of the ad needs to be under 30 characters. The description needs to be under 90 characters. Format the output as a table.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_73",
|
||||
"name": "Customer Case Study",
|
||||
"content": "Write a customer case study highlighting how [company name] used [product name] to [achieve success]. Include 4 customer quotes, 2 customer success metrics, and visual elements.\n\nCase details:\n[additional details go here]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_74",
|
||||
"name": "Event Invite",
|
||||
"content": "Write a persuasive email to increase attendance at our upcoming event about [theme].\n\nHere are additional details about the event to include:\n[event details]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_75",
|
||||
"name": "Internal Memos",
|
||||
"content": "Write an internal memo to [specific department/team] regarding [topic].\n\nIn the memo, explain [key details] and the desired outcome. Provide an action plan with clear steps and timeline.",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_76",
|
||||
"name": "Pitch a Journalist",
|
||||
"content": "Pitch a story to [reporter name/publication].\n\nStory details: [details]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_77",
|
||||
"name": "Press Release",
|
||||
"content": "Write a press release for [company or organization] announcing a recent achievement or milestone. Begin with a concise and attention-grabbing headline that summarizes the main news. In the opening paragraph, provide a brief overview of the announcement and its significance. In the following paragraphs, provide more details about the achievement, including any relevant statistics or data. Use quotes from company representatives or experts to add credibility and context to the announcement. End the press release with a brief company description and contact information for media inquiries.\n\nRemember to keep the press release concise, informative, and engaging, highlighting the key takeaways and benefits of the announcement.\n\nPress release details:\n\n[paste any necessary details about the announcement here]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_78",
|
||||
"name": "Linkedin Connection Invite Message",
|
||||
"content": "You are a recruiter trying to attract top talent.\n\nYou came across this linkedin profile [linkedin URL].\n\nYou have to pitch them and you can't write more than 500 characters\n\nYour message should include:\n- the name of the company they're currently working for\n- praise for their areas of expertise",
|
||||
"category": "recruiting"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_79",
|
||||
"name": "3 Step Outreach Sequence",
|
||||
"content": "Generate a 3 step outreach sequence for [company URL] selling to [target customer]",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_80",
|
||||
"name": "Analyze Industry Trends",
|
||||
"content": "Analyze the industry trends for [industry] in the [country/region] for the past 12 months and compare it to the same period last year.\n\nPresent your findings in a report.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_81",
|
||||
"name": "Brainstorm Pain Points",
|
||||
"content": "Act as a [target persona].\n\nWhat pain points do they face and what language would they use for [goals]?",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_82",
|
||||
"name": "Competitive Analysis",
|
||||
"content": "Conduct a full analysis of [competitor company name] and identify the competitive advantages and disadvantages of their product.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_83",
|
||||
"name": "Linkedin Boolean Search",
|
||||
"content": "Write me a boolean search on variations of [roles] that I can plug into sales navigator search",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_84",
|
||||
"name": "Personalized Cold Email From LinkedIn Profile",
|
||||
"content": "Write a personalized cold email to [linkedin profile url] selling [product description].\n\nMake sure the first 7 words of the email catch the reader's attention. Make the email 4-6 sentences.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_85",
|
||||
"name": "Research Prospect From Linkedin",
|
||||
"content": "Summarize [linkedin profile url] into 10 bullet points, brainstorm the pain points they have around [topic]",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_86",
|
||||
"name": "Caption Generator",
|
||||
"content": "Write a catchy caption about [your theme] and try to play with words to make it fun, engage users in the end, ask them questions, use relevant emojis, and 3 hashtags in the end",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_87",
|
||||
"name": "Generate Content Calendar",
|
||||
"content": "Generate a content calendar about [topic]",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_88",
|
||||
"name": "Headlines",
|
||||
"content": "Write 5 attention-grabbing headlines for a [platform] post on [topic] for [audience].",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_89",
|
||||
"name": "Instagram Captions",
|
||||
"content": "Write 5 variations of Instagram captions for [product].\n\nUse friendly, human-like language that appeals to [target audience].\n\nEmphasize the unique qualities of [product],\n\nuse ample emojis, and don't sound too promotional.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_90",
|
||||
"name": "LinkedIn Post",
|
||||
"content": "Create a narrative Linkedin post using immersive writing about [topic].\n\nDetails:\n\n[give details in bullet point format]\n\nUse a mix of short and long sentences. Make it punchy and dramatic.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_91",
|
||||
"name": "TikTok Script",
|
||||
"content": "Write a super engaging TikTok video script on [topic]. Each sentence should catch the viewer's attention to make them keep watching.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_92",
|
||||
"name": "Twitter Thread",
|
||||
"content": "Give a controversial opinion on [topic], then turn it into a twitter thread.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_93",
|
||||
"name": "Youtube Video Description",
|
||||
"content": "Write a 100-word YouTube video description that compels [audience]\n\nto watch a video on [topic]\n\nand mentions the following keywords\n\n[keyword 1]\n\n[keyword 2]\n\n[keyword 3].",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_94",
|
||||
"name": "Reframing Business Perspectives",
|
||||
"content": "Apply Reframing Business Perspectives to analyze [my business decision]. Look at the problem from different angles, challenging the existing beliefs.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_95",
|
||||
"name": "Behavioral Economics Principles",
|
||||
"content": "Apply Behavioral Economics Principles to analyze [my business decision]. Consider how cognitive, emotional, and social factors affect economic decisions.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_96",
|
||||
"name": "Blue Ocean Strategy",
|
||||
"content": "Apply the Blue Ocean Strategy to evaluate [my business decision]. Focus on creating uncontested market space rather than competing in existing industries.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_97",
|
||||
"name": "Brand Ecosystem Development",
|
||||
"content": "Utilize Brand Ecosystem Development to assess [my business decision]. Build a network of interrelated products, services, and stakeholders.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_98",
|
||||
"name": "Consumer Behavior Analysis",
|
||||
"content": "Use Consumer Behavior Analysis to evaluate [my business decision]. Dive into the psychological, personal, and social influences on consumer choices.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_99",
|
||||
"name": "Cost-Benefit Analysis",
|
||||
"content": "Apply Cost-Benefit Analysis to assess [my business decision]. Analyze the expected balance of benefits and costs, including possible risk and uncertainties.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_100",
|
||||
"name": "Customer Lifetime Value",
|
||||
"content": "Assess [my business decision] by considering Customer Lifetime Value. Analyze the long-term value of customers to understand how acquisition, retention, and monetization strategies align.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_101",
|
||||
"name": "Customer Persona Building",
|
||||
"content": "Utilize Customer Persona Building to evaluate [my business decision]. Define specific customer archetypes with detailed attributes and needs.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_102",
|
||||
"name": "Disruptive Innovation",
|
||||
"content": "Apply Disruptive Innovation to assess [my business decision]. Consider how groundbreaking changes in technology or methodology might impact your industry or market.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_103",
|
||||
"name": "Double Loop Learning",
|
||||
"content": "Use Double Loop Learning to evaluate [my business decision]. Reflect not just on solutions, but on underlying assumptions and beliefs, encouraging adaptive learning.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_104",
|
||||
"name": "Eisenhower Matrix",
|
||||
"content": "Use the Eisenhower Matrix to evaluate [my business decision]. Categorize tasks or elements based on urgency and importance to prioritize effectively.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_105",
|
||||
"name": "Emotional Intelligence",
|
||||
"content": "Evaluate [my business decision] with Emotional Intelligence in mind. Recognize and manage both your own and others' emotions to make more empathetic and effective decisions.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_106",
|
||||
"name": "Freemium Business Model",
|
||||
"content": "Apply the Freemium Business Model to [my business decision]. Explore offering basic services for free while charging for premium features.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_107",
|
||||
"name": "Heuristics and Decision Trees",
|
||||
"content": "Evaluate [my business decision] using Heuristics and Decision Trees. Create simplified models to understand complex problems and find optimal paths.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_108",
|
||||
"name": "Hyper-Personalization Strategy",
|
||||
"content": "Use Hyper-Personalization Strategy to evaluate [my business decision]. Leverage data and AI to provide an extremely personalized experience.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_109",
|
||||
"name": "Innovation Ambition Matrix",
|
||||
"content": "Evaluate [my business decision] through the Innovation Ambition Matrix. Plot initiatives on a matrix to balance core enhancements, adjacent opportunities, and transformational innovations.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_110",
|
||||
"name": "Jobs to Be Done Framework",
|
||||
"content": "Assess [my business decision] by applying the Jobs to Be Done Framework. Focus on the problems customers are trying to solve.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_111",
|
||||
"name": "Kano Model Analysis",
|
||||
"content": "Evaluate [my business decision] using the Kano Model Analysis. Prioritize customer needs into basic, performance, and excitement categories.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_112",
|
||||
"name": "Lean Startup Principles",
|
||||
"content": "Apply Lean Startup Principles to [my business decision]. Focus on creating a minimum viable product, measuring its success, and learning from the results.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_113",
|
||||
"name": "Long Tail Strategy",
|
||||
"content": "Analyze [my business decision] focusing on the Long Tail Strategy. Consider how niche markets or products may contribute to overall success.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_114",
|
||||
"name": "Network Effects",
|
||||
"content": "Analyze [my business decision] through the understanding of Network Effects. Consider how the value of a product or service increases as more people use it.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_115",
|
||||
"name": "Pre-Mortem Analysis",
|
||||
"content": "Utilize Pre-Mortem Analysis to assess [my business decision]. Imagine a future failure of the decision and work backward to identify potential causes and mitigation strategies.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_116",
|
||||
"name": "Prospect Theory",
|
||||
"content": "Utilize Prospect Theory to assess [my business decision]. Understand how people perceive gains and losses and how that can influence decision-making.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_117",
|
||||
"name": "Pygmalion Effect",
|
||||
"content": "Apply the Pygmalion Effect to analyze [my business decision]. Recognize how expectations can influence outcomes, both positively and negatively.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_118",
|
||||
"name": "Resource-Based View",
|
||||
"content": "Apply the Resource-Based View to evaluate [my business decision]. Focus on leveraging the company's internal strengths and weaknesses in relation to external opportunities and threats.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_119",
|
||||
"name": "Risk-Reward Analysis",
|
||||
"content": "Analyze [my business decision] through Risk-Reward Analysis. Evaluate the potential risks against the potential rewards to understand the balance and make an informed decision.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_120",
|
||||
"name": "Focus on Results",
|
||||
"content": "Can you provide me with a case study template for [CLIENT NAME]'s [PRODUCT/SERVICE] that showcases the results achieved? Please suggest the key metrics and KPIs that should be included. Use [AGENCY NAME], [RESULTS], and [KEY METRICS] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_121",
|
||||
"name": "Showcase Digital Transformation",
|
||||
"content": "How would you structure a case study for [CLIENT NAME] that focuses on their journey of digital transformation? Please suggest the main sections, such as the challenges, the solutions, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [OUTCOMES] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_122",
|
||||
"name": "Highlighting Collaboration",
|
||||
"content": "Can you generate a case study template for [CLIENT NAME] that highlights their partnership with [PARTNER NAME]? Please include details on the collaboration process, the benefits for both parties, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [PARTNER NAME] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_123",
|
||||
"name": "Achieving Goals",
|
||||
"content": "How would you structure a case study for [CLIENT NAME] that showcases their success in achieving sustainability goals? Please suggest the main sections, such as the challenges, the solutions, and the impact achieved. Use [AGENCY NAME], [CLIENT NAME], and [IMPACT] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_124",
|
||||
"name": "Focus on Strategy",
|
||||
"content": "Can you provide me with a case study template for [CLIENT NAME] that highlights their social media strategy? Please include details on the objectives, the tactics, and the results achieved. Use [AGENCY NAME], [CLIENT NAME], and [RESULTS] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_125",
|
||||
"name": "Highlight Unique Features",
|
||||
"content": "Can you write a benefit-driven sales copy that highlights the unique features and benefits of [product/service]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_126",
|
||||
"name": "Explain the Benefits",
|
||||
"content": "How would you describe the benefits of [product/service] to a potential customer in a way that would convince them to purchase?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_127",
|
||||
"name": "Explain the Need",
|
||||
"content": "Can you craft a benefit-driven sales copy that explains why [product/service] is the best solution for [customer's problem or need]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_128",
|
||||
"name": "Crafting Convincing Copy",
|
||||
"content": "How would you sell the benefits of [product/service] to a customer who is on the fence about making a purchase?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_129",
|
||||
"name": "Overcome Objections",
|
||||
"content": "How would you sell the benefits of [product/service] to a customer who is skeptical about its effectiveness for [customer's problem or need]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_130",
|
||||
"name": "Make Your Product Stand Out (Long-Form)",
|
||||
"content": "Can you write an extended sales copy describing the unique features and benefits of [product/service] that sets it apart from the competition and makes it a must-have for [target audience]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_131",
|
||||
"name": "Top Features (Long-Form)",
|
||||
"content": "Can you write an extended sales copy on what are the top features of [product/service] that [target audience] will love?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_132",
|
||||
"name": "Explaining the Process (Long-Form)",
|
||||
"content": "Can you write an extended sales copy explaining in detail the process of using [product/service] and how it can help [target audience] achieve their goals?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_133",
|
||||
"name": "Differentiating Your Product (Long-Form)",
|
||||
"content": "Can you write an extended sales copy on what makes [product/service] stand out from similar offerings in the market, and why should [target audience] choose it over the competition?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_134",
|
||||
"name": "Convincing Sales Copy (Long-Form)",
|
||||
"content": "Can you write an extended sales copy convincing me why [product/service] is the missing piece [target audience] needs to take their [aspect of life/business] to the next level?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_135",
|
||||
"name": "Engaging Sales Pitches (Medium-Form)",
|
||||
"content": "Write a sales pitch for a [product/service] that highlights its unique features and benefits.",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_136",
|
||||
"name": "Persuasive Emails (Medium-Form)",
|
||||
"content": "Write a persuasive email to a potential customer explaining why they should choose [company name] for their [product/service] needs.",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_137",
|
||||
"name": "Persuasive Blog Posts (Medium-Form)",
|
||||
"content": "Write a persuasive blog post about the benefits of using [product/service] for [specific industry/target market].",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_138",
|
||||
"name": "Problem-Solving Sales Pitches (Medium-Form)",
|
||||
"content": "Write a sales pitch for a [product or service] that emphasizes its unique features and benefits. Include specific details about how it can solve a problem or improve the customer's life.",
|
||||
"category": "salesCopy"
|
||||
}
|
||||
]
|
||||
}
|
||||
Whitespace-only changes.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM9.25 3.75C9.25 4.44036 8.69036 5 8 5C7.30964 5 6.75 4.44036 6.75 3.75C6.75 3.05964 7.30964 2.5 8 2.5C8.69036 2.5 9.25 3.05964 9.25 3.75ZM12 8H9.41901L11.2047 13H9.081L8 9.97321L6.91901 13H4.79528L6.581 8H4V6H12V8Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
output: dist/
|
||||
releases:
|
||||
- name: dev
|
||||
jobs:
|
||||
- name: release-dev-linux-deb
|
||||
package:
|
||||
platform: linux
|
||||
target: deb
|
||||
- name: release-dev-linux-rpm
|
||||
package:
|
||||
platform: linux
|
||||
target: rpm
|
||||
@@ -0,0 +1,36 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT
|
||||
rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG
|
||||
4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw
|
||||
+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV
|
||||
KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5
|
||||
b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z
|
||||
QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW
|
||||
YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG
|
||||
G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu
|
||||
6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA
|
||||
6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp
|
||||
q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd
|
||||
0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/
|
||||
4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb
|
||||
K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7
|
||||
hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO
|
||||
s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz
|
||||
Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4
|
||||
uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV
|
||||
Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn
|
||||
ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB
|
||||
+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN
|
||||
C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r
|
||||
vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx
|
||||
k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y
|
||||
GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/
|
||||
eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG
|
||||
hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM
|
||||
EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8
|
||||
iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI
|
||||
7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb
|
||||
w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf
|
||||
1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P
|
||||
Y29SB4jvwqls268rP0cWqy4WXwlVwuc=
|
||||
-----END PUBLIC KEY-----
|
||||
+13
-30
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database/board/presentation/widgets/board_colum
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -24,24 +23,24 @@ void main() {
|
||||
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
|
||||
|
||||
// Is expanded by default
|
||||
expect(collapseFinder, findsOneWidget);
|
||||
expect(expandFinder, findsNothing);
|
||||
|
||||
// Collapse hidden groups
|
||||
await tester.tap(collapseFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is collapsed
|
||||
expect(collapseFinder, findsNothing);
|
||||
expect(expandFinder, findsOneWidget);
|
||||
|
||||
// Expand hidden groups
|
||||
// Collapse hidden groups
|
||||
await tester.tap(expandFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is expanded
|
||||
// Is collapsed
|
||||
expect(collapseFinder, findsOneWidget);
|
||||
expect(expandFinder, findsNothing);
|
||||
|
||||
// Expand hidden groups
|
||||
await tester.tap(collapseFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is expanded
|
||||
expect(collapseFinder, findsNothing);
|
||||
expect(expandFinder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hide first group, and show it again', (tester) async {
|
||||
@@ -49,6 +48,9 @@ void main() {
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
|
||||
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
|
||||
await tester.tapButton(expandFinder);
|
||||
|
||||
// Tap the options of the first group
|
||||
final optionsFinder = find
|
||||
.descendant(
|
||||
@@ -121,22 +123,3 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension FlowySvgFinder on CommonFinders {
|
||||
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
|
||||
}
|
||||
|
||||
class _FlowySvgFinder extends MatchFinder {
|
||||
_FlowySvgFinder(this.svg);
|
||||
|
||||
final FlowySvgData svg;
|
||||
|
||||
@override
|
||||
String get description => 'flowy_svg "$svg"';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
final Widget widget = candidate.widget;
|
||||
return widget is FlowySvg && widget.svg == svg;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
@@ -14,6 +15,31 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('board row test', () {
|
||||
testWidgets('edit item in ToDo card', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
const name = 'Card 1';
|
||||
final card1 = find.ancestor(
|
||||
matching: find.byType(RowCard),
|
||||
of: find.text(name),
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
card1,
|
||||
onHover: () async {
|
||||
final editCard = find.byType(EditCardAccessory);
|
||||
await tester.tapButton(editCard);
|
||||
},
|
||||
);
|
||||
await tester.showKeyboard(card1);
|
||||
tester.testTextInput.enterText("");
|
||||
await tester.pump(300.milliseconds);
|
||||
tester.testTextInput.enterText("a");
|
||||
await tester.pump(300.milliseconds);
|
||||
expect(find.text('a'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('delete item in ToDo card', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'database/database_test_runner.dart' as database_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
import 'uncategorized/uncategorized_test_runner.dart'
|
||||
as uncategorized_test_runner;
|
||||
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
|
||||
import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner;
|
||||
|
||||
Future<void> main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
|
||||
data_migration_test_runner.main();
|
||||
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
|
||||
@@ -18,5 +27,9 @@ Future<void> main() async {
|
||||
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
|
||||
// database
|
||||
database_test_runner.main();
|
||||
}
|
||||
-30
@@ -1,33 +1,10 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
import '../../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -38,7 +15,6 @@ void main() {
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
|
||||
await tester.tapContinousAnotherWay();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
@@ -54,12 +30,6 @@ void main() {
|
||||
await tester.enterUserName('local_user');
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(AccountSignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
// sign up with Google
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
import 'anon_user_data_migration_test.dart' as anon_user_test;
|
||||
|
||||
void main() async {
|
||||
anon_user_test.main();
|
||||
// anon_user_test.main();
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// copy link to block
|
||||
group('database image:', () {
|
||||
testWidgets('insert image', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open the first row detail page and upload an image
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Grid,
|
||||
pageName: 'database image',
|
||||
);
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// insert an image block
|
||||
{
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
// upload an image
|
||||
{
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(
|
||||
paths: [imagePath],
|
||||
);
|
||||
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
|
||||
// remove the temp file
|
||||
file.deleteSync();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'database_image_test.dart' as database_image_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
database_image_test.main();
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('AI Writer:', () {
|
||||
testWidgets('the ai writer transaction should only apply in memory',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
|
||||
);
|
||||
expect(find.byType(AiWriterBlockComponent), findsOneWidget);
|
||||
|
||||
// switch to another page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
// switch back to the page
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// expect the ai writer block is not in the document
|
||||
expect(find.byType(AiWriterBlockComponent), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// copy link to block
|
||||
group('copy link to block:', () {
|
||||
testWidgets('copy link to check if the clipboard has the correct content',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open getting started page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
await tester.editor.copyLinkToBlock([0]);
|
||||
await tester.pumpAndSettle(Durations.short1);
|
||||
|
||||
// check the clipboard
|
||||
final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
expect(
|
||||
content?.text,
|
||||
matches(appflowySharePageLinkPattern),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('copy link to block(another page) and paste it in doc',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open getting started page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
await tester.editor.copyLinkToBlock([0]);
|
||||
|
||||
// create a new page and paste it
|
||||
const pageName = 'copy link to block';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// paste the link to the new page
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.paste();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the content of the block
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
final delta = node.delta!;
|
||||
final insert = (delta.first as TextInsert).text;
|
||||
final attributes = delta.first.attributes;
|
||||
expect(insert, MentionBlockKeys.mentionChar);
|
||||
final mention =
|
||||
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
|
||||
expect(mention[MentionBlockKeys.type], MentionType.page.name);
|
||||
expect(mention[MentionBlockKeys.blockId], isNotNull);
|
||||
expect(mention[MentionBlockKeys.pageId], isNotNull);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.textContaining(
|
||||
Constants.gettingStartedPageName,
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.textContaining(
|
||||
// the pasted block content is 'Welcome to AppFlowy'
|
||||
'Welcome to AppFlowy',
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// tap the mention block to jump to the page
|
||||
await tester.tapButton(find.byType(MentionPageBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to go to the getting started page
|
||||
final documentPage = find.byType(DocumentPage);
|
||||
expect(documentPage, findsOneWidget);
|
||||
expect(
|
||||
tester.widget<DocumentPage>(documentPage).view.name,
|
||||
Constants.gettingStartedPageName,
|
||||
);
|
||||
// and the block is selected
|
||||
expect(
|
||||
tester.widget<DocumentPage>(documentPage).initialBlockId,
|
||||
mention[MentionBlockKeys.blockId],
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().selection,
|
||||
Selection.collapsed(
|
||||
Position(
|
||||
path: [0],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('copy link to block(same page) and paste it in doc',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// create a new page and paste it
|
||||
const pageName = 'copy link to block';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// copy the link to block from the first line
|
||||
const inputText = 'Hello World';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(inputText);
|
||||
await tester.ime.insertCharacter('\n');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.copyLinkToBlock([0]);
|
||||
|
||||
// paste the link to the second line
|
||||
await tester.editor.tapLineOfEditorAt(1);
|
||||
await tester.editor.paste();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the content of the block
|
||||
final node = tester.editor.getNodeAtPath([1]);
|
||||
final delta = node.delta!;
|
||||
final insert = (delta.first as TextInsert).text;
|
||||
final attributes = delta.first.attributes;
|
||||
expect(insert, MentionBlockKeys.mentionChar);
|
||||
final mention =
|
||||
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
|
||||
expect(mention[MentionBlockKeys.type], MentionType.page.name);
|
||||
expect(mention[MentionBlockKeys.blockId], isNotNull);
|
||||
expect(mention[MentionBlockKeys.pageId], isNotNull);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.textContaining(
|
||||
inputText,
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
// edit the pasted block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('!');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the content of the block
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.textContaining(
|
||||
'$inputText!',
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
// tap the mention block
|
||||
await tester.tapButton(find.byType(MentionPageBlock));
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().selection,
|
||||
Selection.collapsed(
|
||||
Position(
|
||||
path: [0],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('''1. copy link to block from another page
|
||||
2. paste the link to the new page
|
||||
3. delete the original page
|
||||
4. check the content of the block, it should be no access to the page
|
||||
''', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open getting started page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
await tester.editor.copyLinkToBlock([0]);
|
||||
|
||||
// create a new page and paste it
|
||||
const pageName = 'copy link to block';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// paste the link to the new page
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.paste();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// tap the mention block to jump to the page
|
||||
await tester.tapButton(find.byType(MentionPageBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to go to the getting started page
|
||||
final documentPage = find.byType(DocumentPage);
|
||||
expect(documentPage, findsOneWidget);
|
||||
expect(
|
||||
tester.widget<DocumentPage>(documentPage).view.name,
|
||||
Constants.gettingStartedPageName,
|
||||
);
|
||||
// delete the getting started page
|
||||
await tester.hoverOnPageName(
|
||||
Constants.gettingStartedPageName,
|
||||
onHover: () async => tester.tapDeletePageButton(),
|
||||
);
|
||||
tester.expectToSeeDocumentBanner();
|
||||
tester.expectNotToSeePageName(gettingStarted);
|
||||
|
||||
// delete the page permanently
|
||||
await tester.tapDeletePermanentlyButton();
|
||||
|
||||
// go back the page
|
||||
await tester.openPage(pageName);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the content of the block
|
||||
// it should be no access to the page
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.findTextInFlowyText(
|
||||
LocaleKeys.document_mention_noAccess.tr(),
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-33
@@ -57,7 +57,7 @@ void main() {
|
||||
// move the checkbox to the child of the block at path [9]
|
||||
await tester.editor.dragBlock(
|
||||
[10],
|
||||
const Offset(80, -30),
|
||||
const Offset(120, -20),
|
||||
);
|
||||
|
||||
// wait for the move animation to complete
|
||||
@@ -68,38 +68,6 @@ void main() {
|
||||
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
|
||||
});
|
||||
|
||||
// Copy link to block feature is disable temporarily, enable this test when the feature is ready.
|
||||
// testWidgets('copy block link', (tester) async {
|
||||
// await tester.initializeAppFlowy(
|
||||
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
// );
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// // open getting started page
|
||||
// await tester.openPage(Constants.gettingStartedPageName);
|
||||
|
||||
// // hover and click on the option menu button beside the block component.
|
||||
// await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
// // click the copy link to block option
|
||||
// await tester.tap(
|
||||
// find.findTextInFlowyText(
|
||||
// LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
|
||||
// ),
|
||||
// );
|
||||
// await tester.pumpAndSettle(Durations.short1);
|
||||
|
||||
// // check the clipboard
|
||||
// final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
// expect(
|
||||
// content?.text,
|
||||
// matches(
|
||||
// r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
|
||||
testWidgets('hover on the block and delete it', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_menu.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Publish:', () {
|
||||
testWidgets('publish document', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// open the publish menu
|
||||
await tester.openPublishMenu();
|
||||
|
||||
// publish the document
|
||||
final publishButton = find.byType(PublishButton);
|
||||
final unpublishButton = find.byType(UnPublishButton);
|
||||
await tester.tapButton(publishButton);
|
||||
|
||||
// expect to see unpublish, visit site and manage all sites button
|
||||
expect(unpublishButton, findsOneWidget);
|
||||
expect(find.text(LocaleKeys.shareAction_visitSite.tr()), findsOneWidget);
|
||||
|
||||
// unpublish the document
|
||||
await tester.tapButton(unpublishButton);
|
||||
|
||||
// expect to see publish button
|
||||
expect(publishButton, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('rename path name', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// open the publish menu
|
||||
await tester.openPublishMenu();
|
||||
|
||||
// publish the document
|
||||
final publishButton = find.byType(PublishButton);
|
||||
await tester.tapButton(publishButton);
|
||||
|
||||
// rename the path name
|
||||
final inputField = find.descendant(
|
||||
of: find.byType(ShareMenu),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
// rename with invalid name
|
||||
await tester.tap(inputField);
|
||||
await tester.enterText(inputField, '&&&&????');
|
||||
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with error message
|
||||
final errorToast1 = find.text(
|
||||
LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters
|
||||
.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(errorToast1);
|
||||
await tester.pumpUntilNotFound(errorToast1);
|
||||
|
||||
// rename with long name
|
||||
await tester.tap(inputField);
|
||||
await tester.enterText(inputField, 'long-path-name' * 200);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with error message
|
||||
final errorToast2 = find.text(
|
||||
LocaleKeys.settings_sites_error_publishNameTooLong.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(errorToast2);
|
||||
await tester.pumpUntilNotFound(errorToast2);
|
||||
|
||||
// rename with empty name
|
||||
await tester.tap(inputField);
|
||||
await tester.enterText(inputField, '');
|
||||
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with error message
|
||||
final errorToast3 = find.text(
|
||||
LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(errorToast3);
|
||||
await tester.pumpUntilNotFound(errorToast3);
|
||||
|
||||
// input the new path name
|
||||
await tester.tap(inputField);
|
||||
await tester.enterText(inputField, 'new-path-name');
|
||||
// click save button
|
||||
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with success message
|
||||
final successToast = find.text(
|
||||
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(successToast);
|
||||
await tester.pumpUntilNotFound(successToast);
|
||||
|
||||
// click the copy link button
|
||||
await tester.tapButton(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowySvg &&
|
||||
widget.svg.path == FlowySvgs.m_toolbar_link_m.path,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// check the clipboard has the link
|
||||
final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
expect(
|
||||
content?.text?.contains('new-path-name'),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('re-publish the document', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// open the publish menu
|
||||
await tester.openPublishMenu();
|
||||
|
||||
// publish the document
|
||||
final publishButton = find.byType(PublishButton);
|
||||
await tester.tapButton(publishButton);
|
||||
|
||||
// rename the path name
|
||||
final inputField = find.descendant(
|
||||
of: find.byType(ShareMenu),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
// input the new path name
|
||||
const newName = 'new-path-name';
|
||||
await tester.enterText(inputField, newName);
|
||||
// click save button
|
||||
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with success message
|
||||
final successToast = find.text(
|
||||
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
);
|
||||
await tester.pumpUntilNotFound(successToast);
|
||||
|
||||
// unpublish the document
|
||||
final unpublishButton = find.byType(UnPublishButton);
|
||||
await tester.tapButton(unpublishButton);
|
||||
|
||||
final unpublishSuccessToast = find.text(
|
||||
LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
);
|
||||
await tester.pumpUntilNotFound(unpublishSuccessToast);
|
||||
|
||||
// re-publish the document
|
||||
await tester.tapButton(publishButton);
|
||||
|
||||
// expect to see the toast with success message
|
||||
final rePublishSuccessToast = find.text(
|
||||
LocaleKeys.publish_publishSuccessfully.tr(),
|
||||
);
|
||||
await tester.pumpUntilNotFound(rePublishSuccessToast);
|
||||
|
||||
// check the clipboard has the link
|
||||
final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
expect(
|
||||
content?.text?.contains(newName),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+7
@@ -1,9 +1,16 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_ai_writer_test.dart' as document_ai_writer_test;
|
||||
import 'document_copy_link_to_block_test.dart'
|
||||
as document_copy_link_to_block_test;
|
||||
import 'document_option_actions_test.dart' as document_option_actions_test;
|
||||
import 'document_publish_test.dart' as document_publish_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
document_option_actions_test.main();
|
||||
document_copy_link_to_block_test.main();
|
||||
document_publish_test.main();
|
||||
document_ai_writer_test.main();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
// This test is meaningless, just for preventing the CI from failing.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Empty', () {
|
||||
testWidgets('set appflowy cloud', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('Change slide bar space icon', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(SidebarSpaceHeader),
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(SpaceMorePopup);
|
||||
await tester.tapButton(moreOption);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsNothing);
|
||||
await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final icons = find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
);
|
||||
expect(icons, findsOneWidget);
|
||||
await tester.tapIcon(EmojiIconData.icon(firstIcon));
|
||||
|
||||
final spaceHeader = find.byType(SidebarSpaceHeader);
|
||||
final spaceIcon = find.descendant(
|
||||
of: spaceHeader,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
),
|
||||
);
|
||||
expect(spaceIcon, findsOneWidget);
|
||||
});
|
||||
}
|
||||
+9
-22
@@ -1,34 +1,13 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -92,6 +71,14 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Attempt right-click on the page name and expect not to see
|
||||
await tester.tap(gettingStarted.last, buttons: kSecondaryButton);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(LocaleKeys.disclosureAction_moveTo.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// move the current page to Getting started
|
||||
await tester.tapButton(
|
||||
gettingStarted.last,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Rename empty name view (untitled)', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// click the ... button and open rename dialog
|
||||
await tester.hoverOnPageName(
|
||||
ViewLayoutPB.Document.defaultName,
|
||||
onHover: () async {
|
||||
await tester.tapPageOptionButton();
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.disclosureAction_rename.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(NavigatorTextFieldDialog), findsOneWidget);
|
||||
|
||||
final textField = tester.widget<FlowyFormTextInput>(
|
||||
find.descendant(
|
||||
of: find.byType(NavigatorTextFieldDialog),
|
||||
matching: find.byType(FlowyFormTextInput),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
textField.controller!.text,
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
});
|
||||
}
|
||||
-15
@@ -1,21 +1,12 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -66,12 +57,6 @@ void main() {
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(AccountSignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
|
||||
-15
@@ -1,24 +1,9 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
// This test is meaningless, just for preventing the CI from failing.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Empty', () {
|
||||
testWidgets('set appflowy cloud', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
-2
@@ -1,9 +1,7 @@
|
||||
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
|
||||
import 'empty_test.dart' as preset_af_cloud_env_test;
|
||||
import 'user_setting_sync_test.dart' as user_sync_test;
|
||||
|
||||
void main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
appflowy_cloud_auth_test.main();
|
||||
user_sync_test.main();
|
||||
}
|
||||
-22
@@ -1,33 +1,11 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
import '../../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
+4
-9
@@ -1,21 +1,12 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
import '../../../shared/workspace.dart';
|
||||
|
||||
@@ -49,6 +40,10 @@ void main() {
|
||||
await tester.changeWorkspaceIcon(icon);
|
||||
await tester.changeWorkspaceName(name);
|
||||
|
||||
await tester.pumpUntilNotFound(
|
||||
find.text(LocaleKeys.workspace_renameSuccess.tr()),
|
||||
);
|
||||
|
||||
workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
|
||||
+136
-21
@@ -1,38 +1,20 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('collaborative workspace: ', () {
|
||||
group('collaborative workspace:', () {
|
||||
// combine the create and delete workspace test to reduce the time
|
||||
testWidgets('create a new workspace, open it and then delete it',
|
||||
(tester) async {
|
||||
@@ -93,5 +75,138 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('check the member count immediately after creating a workspace',
|
||||
(tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const name = 'AppFlowy.IO';
|
||||
// the workspace will be opened after created
|
||||
await tester.createCollaborativeWorkspace(name);
|
||||
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
|
||||
// expect to see the member count
|
||||
final memberCount = find.text('1 member');
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const name = 'AppFlowy.IO';
|
||||
// the workspace will be opened after created
|
||||
await tester.createCollaborativeWorkspace(name);
|
||||
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
|
||||
// hover on the workspace and click the more button
|
||||
final workspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
|
||||
);
|
||||
|
||||
// the workspace menu shouldn't conflict with logout
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
final logoutButton = find.byType(WorkspaceMoreButton);
|
||||
await tester.tapButton(logoutButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
|
||||
expect(moreButton, findsNothing);
|
||||
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
|
||||
expect(moreButton, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more action button for the same workspace shouldn't do
|
||||
// anything
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
// click it again
|
||||
await tester.tapButton(moreButton);
|
||||
|
||||
// nothing should happen
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more button of another workspace should close the menu
|
||||
// for this one
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final otherWorspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
|
||||
);
|
||||
final otherMoreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
otherWorspaceItem,
|
||||
onHover: () async {
|
||||
expect(otherMoreButton, findsOneWidget);
|
||||
await tester.tapButton(otherMoreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
expect(moreButton, findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+3
-14
@@ -1,28 +1,17 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_menu.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
import '../../../shared/workspace.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -61,11 +50,11 @@ void main() {
|
||||
final plainText = clipboardContent.plainText;
|
||||
expect(
|
||||
plainText,
|
||||
startsWith(ShareConstants.shareBaseUrl),
|
||||
matches(appflowySharePageLinkPattern),
|
||||
);
|
||||
|
||||
final shareValues = plainText!
|
||||
.replaceAll('${ShareConstants.shareBaseUrl}/', '')
|
||||
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
|
||||
.split('/');
|
||||
final workspaceId = shareValues[0];
|
||||
expect(workspaceId, isNotEmpty);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Tabs', () {
|
||||
testWidgets('close other tabs before opening a new workspace',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const name = 'AppFlowy.IO';
|
||||
// the workspace will be opened after created
|
||||
await tester.createCollaborativeWorkspace(name);
|
||||
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
// create new tabs in the workspace
|
||||
expect(find.byType(FlowyTab), findsNothing);
|
||||
|
||||
const documentOneName = 'document one';
|
||||
const documentTwoName = 'document two';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: documentOneName,
|
||||
);
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: documentTwoName,
|
||||
);
|
||||
|
||||
/// Open second menu item in a new tab
|
||||
await tester.openAppInNewTab(documentOneName, ViewLayoutPB.Document);
|
||||
|
||||
/// Open third menu item in a new tab
|
||||
await tester.openAppInNewTab(documentTwoName, ViewLayoutPB.Document);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(FlowyTab),
|
||||
),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
// switch to the another workspace
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.pumpUntilFound(items);
|
||||
expect(items, findsNWidgets(2));
|
||||
|
||||
// open the first workspace
|
||||
await tester.tap(items.first);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
expect(find.byType(FlowyTab), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the space view should not be opened', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
expect(find.byType(AppFlowyEditorPage), findsNothing);
|
||||
expect(find.text('Blank page'), findsOne);
|
||||
});
|
||||
});
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/util.dart';
|
||||
import '../../../shared/workspace.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('workspace icon:', () {
|
||||
testWidgets('remove icon from workspace', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openWorkspaceMenu();
|
||||
|
||||
// click the workspace icon
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.byType(WorkspaceIcon),
|
||||
),
|
||||
);
|
||||
// click the remove icon button
|
||||
await tester.tapButton(
|
||||
find.text(LocaleKeys.button_remove.tr()),
|
||||
);
|
||||
|
||||
// nothing should happen
|
||||
expect(
|
||||
find.text(LocaleKeys.workspace_updateIconSuccess.tr()),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+277
-20
@@ -1,36 +1,23 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/dir.dart';
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -93,4 +80,274 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('sites settings:', () {
|
||||
testWidgets(
|
||||
'manage published page, set it as homepage, remove the homepage',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// open the publish menu
|
||||
await tester.openPublishMenu();
|
||||
|
||||
// publish the document
|
||||
await tester.tapButton(find.byType(PublishButton));
|
||||
|
||||
// click empty area to close the publish menu
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
// check if the page is published in sites page
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.sites);
|
||||
// wait the backend return the sites data
|
||||
await tester.wait(1000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
// comment it out because it's not allowed to update the namespace in free plan
|
||||
// // set it to homepage
|
||||
// await tester.tapButton(
|
||||
// find.textContaining(
|
||||
// LocaleKeys.settings_sites_selectHomePage.tr(),
|
||||
// ),
|
||||
// );
|
||||
// await tester.tapButton(
|
||||
// find.descendant(
|
||||
// of: find.byType(SelectHomePageMenu),
|
||||
// matching: find.text(pageName),
|
||||
// ),
|
||||
// );
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// // check if the page is set to homepage
|
||||
// final homePageItem = find.descendant(
|
||||
// of: find.byType(DomainItem),
|
||||
// matching: find.text(pageName),
|
||||
// );
|
||||
// expect(homePageItem, findsOneWidget);
|
||||
|
||||
// // remove the homepage
|
||||
// await tester.tapButton(find.byType(DomainMoreAction));
|
||||
// await tester.tapButton(
|
||||
// find.text(LocaleKeys.settings_sites_removeHomepage.tr()),
|
||||
// );
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// // check if the page is removed from homepage
|
||||
// expect(homePageItem, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('update namespace', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// check if the page is published in sites page
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.sites);
|
||||
// wait the backend return the sites data
|
||||
await tester.wait(1000);
|
||||
|
||||
// update the domain
|
||||
final domainMoreAction = find.byType(DomainMoreAction);
|
||||
await tester.tapButton(domainMoreAction);
|
||||
final updateNamespaceButton = find.text(
|
||||
LocaleKeys.settings_sites_updateNamespace.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(updateNamespaceButton);
|
||||
|
||||
// click the update namespace button
|
||||
|
||||
await tester.tapButton(updateNamespaceButton);
|
||||
|
||||
// comment it out because it's not allowed to update the namespace in free plan
|
||||
// expect to see the dialog
|
||||
// await tester.updateNamespace('&&&???');
|
||||
|
||||
// // need to upgrade to pro plan to update the namespace
|
||||
// final errorToast = find.text(
|
||||
// LocaleKeys.settings_sites_error_proPlanLimitation.tr(),
|
||||
// );
|
||||
// await tester.pumpUntilFound(errorToast);
|
||||
// expect(errorToast, findsOneWidget);
|
||||
// await tester.pumpUntilNotFound(errorToast);
|
||||
|
||||
// comment it out because it's not allowed to update the namespace in free plan
|
||||
// // short namespace
|
||||
// await tester.updateNamespace('a');
|
||||
|
||||
// // expect to see the toast with error message
|
||||
// final errorToast2 = find.text(
|
||||
// LocaleKeys.settings_sites_error_namespaceTooShort.tr(),
|
||||
// );
|
||||
// await tester.pumpUntilFound(errorToast2);
|
||||
// expect(errorToast2, findsOneWidget);
|
||||
// await tester.pumpUntilNotFound(errorToast2);
|
||||
// // valid namespace
|
||||
// await tester.updateNamespace('AppFlowy');
|
||||
|
||||
// // expect to see the toast with success message
|
||||
// final successToast = find.text(
|
||||
// LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
|
||||
// );
|
||||
// await tester.pumpUntilFound(successToast);
|
||||
// expect(successToast, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
More actions for published page:
|
||||
1. visit site
|
||||
2. copy link
|
||||
3. settings
|
||||
4. unpublish
|
||||
5. custom url
|
||||
''', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// open the publish menu
|
||||
await tester.openPublishMenu();
|
||||
|
||||
// publish the document
|
||||
await tester.tapButton(find.byType(PublishButton));
|
||||
|
||||
// click empty area to close the publish menu
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
// check if the page is published in sites page
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.sites);
|
||||
// wait the backend return the sites data
|
||||
await tester.wait(2000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
|
||||
final customUrlItem = find.text(LocaleKeys.settings_sites_customUrl.tr());
|
||||
final unpublishItem = find.text(LocaleKeys.shareAction_unPublish.tr());
|
||||
|
||||
// custom url
|
||||
final publishMoreAction = find.byType(PublishedViewMoreAction);
|
||||
|
||||
// click the copy link button
|
||||
{
|
||||
await tester.tapButton(publishMoreAction);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilFound(copyLinkItem);
|
||||
await tester.tapButton(copyLinkItem);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilNotFound(copyLinkItem);
|
||||
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
final plainText = clipboardContent.plainText;
|
||||
expect(
|
||||
plainText,
|
||||
contains(pageName),
|
||||
);
|
||||
}
|
||||
|
||||
// custom url
|
||||
{
|
||||
await tester.tapButton(publishMoreAction);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilFound(customUrlItem);
|
||||
await tester.tapButton(customUrlItem);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilNotFound(customUrlItem);
|
||||
|
||||
// see the custom url dialog
|
||||
final customUrlDialog = find.byType(PublishedViewSettingsDialog);
|
||||
expect(customUrlDialog, findsOneWidget);
|
||||
|
||||
// rename the custom url
|
||||
final textField = find.descendant(
|
||||
of: customUrlDialog,
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await tester.enterText(textField, 'hello-world');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the save button
|
||||
final saveButton = find.descendant(
|
||||
of: customUrlDialog,
|
||||
matching: find.text(LocaleKeys.button_save.tr()),
|
||||
);
|
||||
await tester.tapButton(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the toast with success message
|
||||
final successToast = find.text(
|
||||
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(successToast);
|
||||
expect(successToast, findsOneWidget);
|
||||
}
|
||||
|
||||
// unpublish
|
||||
{
|
||||
await tester.tapButton(publishMoreAction);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilFound(unpublishItem);
|
||||
await tester.tapButton(unpublishItem);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpUntilNotFound(unpublishItem);
|
||||
|
||||
// expect to see the toast with success message
|
||||
final successToast = find.text(
|
||||
LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
);
|
||||
await tester.pumpUntilFound(successToast);
|
||||
expect(successToast, findsOneWidget);
|
||||
await tester.pumpUntilNotFound(successToast);
|
||||
|
||||
// check if the page is unpublished in sites page
|
||||
expect(pageItem, findsNothing);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'change_name_and_icon_test.dart' as change_name_and_icon_test;
|
||||
import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
|
||||
import 'share_menu_test.dart' as share_menu_test;
|
||||
import 'tabs_test.dart' as tabs_test;
|
||||
import 'workspace_icon_test.dart' as workspace_icon_test;
|
||||
import 'workspace_settings_test.dart' as workspace_settings_test;
|
||||
|
||||
void main() {
|
||||
@@ -12,4 +14,6 @@ void main() {
|
||||
share_menu_test.main();
|
||||
collaborative_workspace_test.main();
|
||||
change_name_and_icon_test.main();
|
||||
workspace_icon_test.main();
|
||||
tabs_test.main();
|
||||
}
|
||||
+104
-8
@@ -1,6 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -8,7 +17,9 @@ import 'package:integration_test/integration_test.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('Folder Search', () {
|
||||
testWidgets('Search for views', (tester) async {
|
||||
@@ -33,21 +44,106 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
|
||||
// Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna)
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
expect(find.byType(SearchResultCell), findsNWidgets(2));
|
||||
|
||||
// The score should be higher for "ViewOna" thus it should be shown first
|
||||
final secondDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(secondDocumentWidget.result.data, secondDocument);
|
||||
.widget(find.byType(SearchResultCell).first) as SearchResultCell;
|
||||
expect(secondDocumentWidget.item.displayName, secondDocument);
|
||||
|
||||
// Change search to "ViewOne"
|
||||
await tester.enterText(searchFieldFinder, firstDocument);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
// The score should be higher for "ViewOne" thus it should be shown first
|
||||
final firstDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
final firstDocumentWidget = tester.widget(
|
||||
find.byType(SearchResultCell).first,
|
||||
) as SearchResultCell;
|
||||
expect(firstDocumentWidget.item.displayName, firstDocument);
|
||||
});
|
||||
|
||||
testWidgets('Displaying icons in search results', (tester) async {
|
||||
final randomValue = Random().nextInt(10000) + 10000;
|
||||
final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create two pages
|
||||
for (final pageName in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: pageName);
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: pageName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
icon: emojiIconData,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
|
||||
/// search for `Page`
|
||||
final searchFieldFinder = find.descendant(
|
||||
of: find.byType(SearchField),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
expect(find.byType(SearchResultCell), findsNWidgets(2));
|
||||
|
||||
/// check results
|
||||
final svgs = find.descendant(
|
||||
of: find.byType(SearchResultCell),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(svgs, findsNWidgets(2));
|
||||
|
||||
final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,
|
||||
lastSvg = svgs.last.evaluate().first.widget as FlowySvg;
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
/// icon displayed correctly
|
||||
expect(firstSvg.svgString, iconData.svgString);
|
||||
expect(lastSvg.svgString, iconData.svgString);
|
||||
|
||||
testWidgets('select the content in document and search', (tester) async {
|
||||
const firstDocument = ''; // empty document
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: firstDocument);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(FloatingToolbar),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
expect(find.byType(CommandPaletteModal), findsOneWidget);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(firstDocument),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -27,11 +27,12 @@ void main() {
|
||||
expect(find.byType(RecentViewsList), findsOneWidget);
|
||||
|
||||
// Expect three recent history items
|
||||
expect(find.byType(RecentViewTile), findsNWidgets(3));
|
||||
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
|
||||
|
||||
// Expect the first item to be the last viewed document
|
||||
final firstDocumentWidget =
|
||||
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
|
||||
tester.widget(find.byType(SearchRecentViewCell).first)
|
||||
as SearchRecentViewCell;
|
||||
expect(firstDocumentWidget.view.name, secondDocument);
|
||||
});
|
||||
});
|
||||
|
||||
+79
-2
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
@@ -277,5 +285,74 @@ void main() {
|
||||
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
|
||||
testWidgets('filter calendar events', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Calendar,
|
||||
);
|
||||
|
||||
// Create a new event on the first of this month
|
||||
final today = DateTime.now();
|
||||
final firstOfThisMonth = DateTime(today.year, today.month);
|
||||
await tester.doubleClickCalendarCell(firstOfThisMonth);
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
await tester.openCalendarEvent(index: 0, date: firstOfThisMonth);
|
||||
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
|
||||
await tester.createOption(name: "asdf");
|
||||
await tester.createOption(name: "qwer");
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
|
||||
|
||||
await tester.tapFilterButtonInGrid('Tags');
|
||||
await tester.tapOptionFilterWithName('asdf');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Tags');
|
||||
await tester.tapOptionFilterWithName('asdf');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Tags');
|
||||
await tester.tapOptionFilterWithName('asdf');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
final secondOfThisMonth = DateTime(today.year, today.month, 2);
|
||||
await tester.doubleClickCalendarCell(secondOfThisMonth);
|
||||
await tester.dismissEventEditor();
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
await tester.openCalendarEvent(index: 0, date: secondOfThisMonth);
|
||||
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Tags');
|
||||
await tester.changeSelectFilterCondition(
|
||||
SelectOptionFilterConditionPB.OptionIsEmpty,
|
||||
);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, secondOfThisMonth);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -211,7 +211,7 @@ void main() {
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
// Select a date
|
||||
final now = DateTime.now();
|
||||
DateTime now = DateTime.now();
|
||||
await tester.selectDay(content: now.day);
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
@@ -225,7 +225,7 @@ void main() {
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
|
||||
// Toggle include time
|
||||
// When toggling include time, the time value is from the previous existing date time, not the current time
|
||||
now = DateTime.now();
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
+41
@@ -15,6 +15,7 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
@@ -29,6 +30,11 @@ void main() {
|
||||
await tester.tapHidePropertyButton();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// create another field, New field 1 to be hidden still
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// go back to inline database view, expect field to be shown
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
tester.findFieldWithName('New field 1');
|
||||
@@ -60,5 +66,40 @@ void main() {
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
|
||||
});
|
||||
|
||||
testWidgets('field cell width', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// check the width of the field
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// change the width of the field
|
||||
await tester.changeFieldWidth('New field 1', 200);
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// create another field, New field 1 to be same width
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// go back to inline database view, expect New field 1 to be 150px
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// go back to linked database view, expect New field 1 to be 205px
|
||||
await tester.tapTabBarLinkedViewByViewName('Grid');
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
});
|
||||
});
|
||||
}
|
||||
+35
-4
@@ -1,8 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
@@ -316,6 +324,30 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('text in viewport while typing', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.changeCalculateAtIndex(0, CalculationType.Count);
|
||||
|
||||
// add very large text with 200 lines
|
||||
final largeText = List.generate(
|
||||
200,
|
||||
(index) => 'Line ${index + 1}',
|
||||
).join('\n');
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 2,
|
||||
fieldType: FieldType.RichText,
|
||||
input: largeText,
|
||||
);
|
||||
|
||||
// checks if last line is in view port
|
||||
tester.expectToSeeText('Line 200');
|
||||
});
|
||||
|
||||
// Disable this test because it fails on CI randomly
|
||||
// testWidgets('last modified and created at field type options',
|
||||
// (tester) async {
|
||||
@@ -513,8 +545,8 @@ void main() {
|
||||
|
||||
// edit the first date cell
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
|
||||
await tester.toggleIncludeTime();
|
||||
final now = DateTime.now();
|
||||
await tester.toggleIncludeTime();
|
||||
await tester.selectDay(content: now.day);
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
@@ -557,7 +589,6 @@ void main() {
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: DateFormat('MMM dd, y').format(now),
|
||||
// content: DateFormat('MMM dd, y HH:mm').format(now),
|
||||
);
|
||||
tester.assertCellContent(
|
||||
rowIndex: 1,
|
||||
|
||||
+35
-10
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
await tester.tapOptionFilterWithName('s4');
|
||||
|
||||
// The row with 's4' should be shown.
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(2);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
@@ -154,12 +154,10 @@ void main() {
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
await tester.tapFilterButtonInGrid('date');
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
|
||||
await tester.changeDateFilterCondition(DateTimeFilterCondition.before);
|
||||
tester.assertNumberOfRowsInGridPage(7);
|
||||
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.isEmpty);
|
||||
await tester.changeDateFilterCondition(DateTimeFilterCondition.isEmpty);
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
@@ -171,8 +169,6 @@ void main() {
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
|
||||
await tester.createField(
|
||||
FieldType.CreatedTime,
|
||||
name: 'Created at',
|
||||
@@ -189,11 +185,40 @@ void main() {
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Created at');
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
|
||||
await tester.changeDateFilterCondition(DateTimeFilterCondition.before);
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create new row when filters don\'t autofill', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.RichText,
|
||||
'Name',
|
||||
);
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Name');
|
||||
await tester
|
||||
.changeTextFilterCondition(TextFilterConditionPB.TextIsNotEmpty);
|
||||
await tester.dismissCellEditor();
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
expect(find.byType(RowDetailPage), findsOneWidget);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('change icon', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
|
||||
/// create board
|
||||
final addButton = find.byType(AddDatabaseViewButton);
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
/// create calendar
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
final databaseTabBarItem = find.byType(DatabaseTabBarItem);
|
||||
expect(databaseTabBarItem, findsNWidgets(3));
|
||||
final gridItem = databaseTabBarItem.first,
|
||||
boardItem = databaseTabBarItem.at(1),
|
||||
calendarItem = databaseTabBarItem.last;
|
||||
|
||||
/// change the icon of grid
|
||||
/// the first tapping is to select specific item
|
||||
/// the second tapping is to show the menu
|
||||
await tester.tapButton(gridItem);
|
||||
await tester.tapButton(gridItem);
|
||||
|
||||
/// change icon
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final gridIcon = find.descendant(
|
||||
of: gridItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final gridIconWidget =
|
||||
gridIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));
|
||||
final gridIconsData =
|
||||
IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));
|
||||
expect(gridIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of board
|
||||
await tester.tapButton(boardItem);
|
||||
await tester.tapButton(boardItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final boardIcon = find.descendant(
|
||||
of: boardItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final boardIconWidget =
|
||||
boardIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final boardIconsData =
|
||||
IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));
|
||||
expect(boardIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of calendar
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final calendarIcon = find.descendant(
|
||||
of: calendarItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final calendarIconWidget =
|
||||
calendarIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final calendarIconsData =
|
||||
IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));
|
||||
expect(calendarIconsData.iconName, iconsData.iconName);
|
||||
});
|
||||
|
||||
testWidgets('change database icon from sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
final viewItem = find.descendant(
|
||||
of: find.byType(SidebarFolder),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is ViewItem && w.view.name == pageName,
|
||||
),
|
||||
);
|
||||
|
||||
/// change icon to emoji
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(FlowySvg),
|
||||
),
|
||||
);
|
||||
await tester.tapEmoji(emoji);
|
||||
final iconWidget = find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
emoji,
|
||||
);
|
||||
|
||||
/// the icon will not be displayed in database item
|
||||
Finder databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(
|
||||
(databaseIcon.evaluate().first.widget as FlowySvg).svg,
|
||||
FlowySvgs.icon_grid_s,
|
||||
);
|
||||
|
||||
/// change emoji to icon
|
||||
await tester.tapButton(iconWidget);
|
||||
await tester.tapIcon(iconData);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
iconData.emoji,
|
||||
);
|
||||
|
||||
databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final databaseIconWidget =
|
||||
databaseIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final databaseIconsData =
|
||||
IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));
|
||||
expect(icon.svgString, databaseIconsData.svgString);
|
||||
expect(icon.color, isNotEmpty);
|
||||
expect(icon.color, databaseIconsData.color);
|
||||
|
||||
/// the icon in database item should not show the color
|
||||
expect(databaseIconWidget.enableColor, false);
|
||||
});
|
||||
}
|
||||
+18
-33
@@ -21,7 +21,6 @@ import 'package:path_provider/path_provider.dart';
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -69,10 +68,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapFileUploadHint();
|
||||
|
||||
// Expect one file
|
||||
expect(find.byType(RenderMedia), findsOneWidget);
|
||||
@@ -85,9 +81,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
await tester.tapFileUploadHint();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect two files
|
||||
@@ -139,10 +133,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapFileUploadHint();
|
||||
|
||||
// Expect two files
|
||||
expect(find.byType(RenderMedia), findsNWidgets(2));
|
||||
@@ -193,10 +184,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapFileUploadHint();
|
||||
|
||||
// Expect two files
|
||||
expect(find.byType(RenderMedia), findsNWidgets(2));
|
||||
@@ -230,7 +218,7 @@ void main() {
|
||||
await Future.wait([firstFile.delete(), secondFile.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('hide file names', (tester) async {
|
||||
testWidgets('show file names', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
@@ -272,10 +260,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapFileUploadHint();
|
||||
|
||||
// Expect two files
|
||||
expect(find.byType(RenderMedia), findsNWidgets(2));
|
||||
@@ -283,28 +268,28 @@ void main() {
|
||||
await tester.dismissCellEditor();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Open first row in row detail view then toggle hide file names
|
||||
// Open first row in row detail view then toggle show file names
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect file names to not be shown (hidden)
|
||||
expect(find.text('sample.jpeg'), findsNothing);
|
||||
expect(find.text('sample.gif'), findsNothing);
|
||||
|
||||
await tester.tapGridFieldWithNameInRowDetailPage('Type');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Toggle show file names
|
||||
await tester.tap(find.byType(Toggle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect file names to be shown
|
||||
expect(find.text('sample.jpeg'), findsOneWidget);
|
||||
expect(find.text('sample.gif'), findsOneWidget);
|
||||
|
||||
await tester.tapGridFieldWithNameInRowDetailPage('Type');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Toggle hide file names
|
||||
await tester.tap(find.byType(Toggle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect file names to be hidden
|
||||
expect(find.text('sample.jpeg'), findsNothing);
|
||||
expect(find.text('sample.gif'), findsNothing);
|
||||
|
||||
// Remove the temp files
|
||||
await Future.wait([firstFile.delete(), secondFile.delete()]);
|
||||
});
|
||||
|
||||
-59
@@ -4,12 +4,10 @@ import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/af_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -26,63 +24,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database row cover', () {
|
||||
testWidgets('add image to media field and check if cover is set (grid)',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Invoke the field editor
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapEditFieldButton();
|
||||
|
||||
// Change to media type
|
||||
await tester.tapSwitchFieldTypeButton();
|
||||
await tester.selectFieldType(FieldType.Media);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Prepare file for upload from local
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(paths: [imagePath]);
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
|
||||
// Open media cell editor
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
|
||||
await tester.findMediaCellEditor(findsOneWidget);
|
||||
|
||||
// Click on add file button in the Media Cell Editor
|
||||
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
|
||||
);
|
||||
|
||||
// Expect one file
|
||||
expect(find.byType(RenderMedia), findsOneWidget);
|
||||
|
||||
// Close cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Open first row in row detail view
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect a cover to be shown
|
||||
expect(find.byType(RowCover), findsOneWidget);
|
||||
|
||||
// Remove the temp file
|
||||
await Future.wait([file.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('add and remove cover from Row Detail Card', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+173
-4
@@ -1,15 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -18,7 +22,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid row detail page:', () {
|
||||
testWidgets('opens', (tester) async {
|
||||
@@ -73,6 +84,24 @@ void main() {
|
||||
// The number of emoji should be two. One in the row displayed in the grid
|
||||
// one in the row detail page.
|
||||
expect(emojiText, findsNWidgets(2));
|
||||
|
||||
// insert a sub page in database
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_subPage_name.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// the row detail page should be closed
|
||||
final rowDetailPage = find.byType(RowDetailPage);
|
||||
await tester.pumpUntilNotFound(rowDetailPage);
|
||||
|
||||
// expect to see a document page
|
||||
final documentPage = find.byType(DocumentPage);
|
||||
expect(documentPage, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('remove emoji', (tester) async {
|
||||
@@ -335,5 +364,145 @@ void main() {
|
||||
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
});
|
||||
|
||||
testWidgets('edit checklist cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType);
|
||||
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(ChecklistRowDetailCell),
|
||||
onHover: () async {
|
||||
await tester.tapButton(find.byType(ChecklistItemControl));
|
||||
},
|
||||
);
|
||||
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 0);
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 1');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 1);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(ChecklistRowDetailCell),
|
||||
onHover: () async {
|
||||
await tester.tapButton(find.byType(ChecklistItemControl));
|
||||
},
|
||||
);
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 2);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(PhantomChecklistItem), findsNothing);
|
||||
|
||||
await tester.renameChecklistTask(index: 0, name: "task -1", enter: false);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task -1",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 1);
|
||||
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 0');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 2);
|
||||
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
expect(find.byType(PhantomChecklistItem), findsNothing);
|
||||
expect(find.byType(ChecklistItem), findsNWidgets(3));
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task -1",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 0",
|
||||
isChecked: true,
|
||||
);
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 2,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(ProgressAndHideCompleteButton),
|
||||
matching: find.byType(FlowyIconButton),
|
||||
),
|
||||
);
|
||||
expect(find.byType(ChecklistItem), findsNWidgets(2));
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task -1",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
await tester.renameChecklistTask(index: 1, name: "task 3", enter: false);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1", enter: false);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 2,
|
||||
name: "task 3",
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 2);
|
||||
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
expect(find.byType(ChecklistItem), findsNWidgets(2));
|
||||
});
|
||||
});
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_settings_test.dart' as database_field_settings_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_field_settings_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_setting_test.main();
|
||||
// DON'T add more tests here.
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_media_test.dart' as database_media_test;
|
||||
import 'database_row_cover_test.dart' as database_row_cover_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
database_filter_test.main();
|
||||
database_sort_test.main();
|
||||
database_view_test.main();
|
||||
database_calendar_test.main();
|
||||
database_media_test.main();
|
||||
database_row_cover_test.main();
|
||||
database_share_test.main();
|
||||
// DON'T add more tests here.
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -73,5 +78,37 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('insert grid in column', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create page and show slash menu
|
||||
await tester.createNewPageWithNameUnderParent(name: 'test page');
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// create a column
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_twoColumns.tr(),
|
||||
);
|
||||
final actionList = find.byType(BlockActionList);
|
||||
expect(actionList, findsNWidgets(2));
|
||||
final position = tester.getCenter(actionList.last);
|
||||
|
||||
/// tap the second child of column
|
||||
await tester.tapAt(position.copyWith(dx: position.dx + 50));
|
||||
|
||||
/// create a grid
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_grid.tr(),
|
||||
);
|
||||
|
||||
final grid = find.byType(GridPageContent);
|
||||
expect(grid, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
+10
-7
@@ -27,8 +27,9 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the align center
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m);
|
||||
|
||||
// expect to see the align center
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
@@ -36,13 +37,15 @@ void main() {
|
||||
expect(first.attributes[blockComponentAlign], 'center');
|
||||
|
||||
// click the align right
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m);
|
||||
expect(first.attributes[blockComponentAlign], 'right');
|
||||
|
||||
// click the align left
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m);
|
||||
expect(first.attributes[blockComponentAlign], 'left');
|
||||
});
|
||||
|
||||
@@ -75,7 +78,7 @@ void main() {
|
||||
[
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.keyE,
|
||||
LogicalKeyboardKey.keyC,
|
||||
],
|
||||
tester: tester,
|
||||
withKeyUp: true,
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Block option interaction tests', () {
|
||||
testWidgets('has correct block selection on tap option button',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// We edit the document by entering some characters, to ensure the document has focus
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [2])),
|
||||
);
|
||||
|
||||
// Insert character 'a' three times - easy to identify
|
||||
await tester.ime.insertText('aaa');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([2]);
|
||||
expect(node?.delta?.toPlainText(), startsWith('aaa'));
|
||||
|
||||
final multiSelection = Selection(
|
||||
start: Position(path: [2], offset: 3),
|
||||
end: Position(path: [4], offset: 40),
|
||||
);
|
||||
|
||||
// Select multiple items
|
||||
await tester.editor.updateSelection(multiSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Press the block option menu
|
||||
await tester.editor.hoverAndClickOptionMenuButton([2]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect the selection to be Block type and not have changed
|
||||
expect(editorState.selectionType, SelectionType.block);
|
||||
expect(editorState.selection, multiSelection);
|
||||
});
|
||||
});
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('callout with emoji icon picker', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
/// create callout
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(),
|
||||
);
|
||||
|
||||
/// select an icon
|
||||
final emojiPickerButton = find.descendant(
|
||||
of: find.byType(CalloutBlockComponentWidget),
|
||||
matching: find.byType(EmojiPickerButton),
|
||||
);
|
||||
await tester.tapButton(emojiPickerButton);
|
||||
await tester.tapIcon(emojiIconData);
|
||||
|
||||
/// verification results
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
final iconWidget = find
|
||||
.descendant(
|
||||
of: emojiPickerButton,
|
||||
matching: find.byType(IconWidget),
|
||||
)
|
||||
.evaluate()
|
||||
.first
|
||||
.widget as IconWidget;
|
||||
final iconWidgetData = iconWidget.iconsData;
|
||||
expect(iconWidgetData.svgString, iconData.svgString);
|
||||
expect(iconWidgetData.iconName, iconData.iconName);
|
||||
expect(iconWidgetData.groupName, iconData.groupName);
|
||||
});
|
||||
}
|
||||
+348
-292
@@ -1,16 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@@ -20,17 +21,20 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('copy and paste in document', () {
|
||||
group('copy and paste in document:', () {
|
||||
testWidgets('paste multiple lines at the first line', (tester) async {
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
await tester.pasteContent(
|
||||
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 3);
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final text =
|
||||
editorState.document.root.children.first.delta!.toPlainText();
|
||||
final textLines = text.split('\n');
|
||||
for (var i = 0; i < lines; i++) {
|
||||
expect(
|
||||
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
||||
textLines[i],
|
||||
'line $i',
|
||||
);
|
||||
}
|
||||
@@ -170,319 +174,370 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
final convertToLinkButton = find.byWidgetPredicate((widget) {
|
||||
return widget is MenuBlockButton &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
|
||||
});
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tap(convertToLinkButton);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'https://appflowy.io/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: bordeLine truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
final pasteAsMenu = find.byType(PasteAsMenu);
|
||||
expect(pasteAsMenu, findsOneWidget);
|
||||
final bookmarkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
|
||||
);
|
||||
await tester.tapButton(bookmarkButton);
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
/// show menu
|
||||
final menu = find.byType(CustomLinkPreviewMenu);
|
||||
expect(menu, findsOneWidget);
|
||||
await tester.tapButton(menu);
|
||||
|
||||
final convertToLinkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl
|
||||
.tr(),
|
||||
);
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
},
|
||||
);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
final pasteAsMenu = find.byType(PasteAsMenu);
|
||||
expect(pasteAsMenu, findsOneWidget);
|
||||
final bookmarkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
|
||||
);
|
||||
await tester.tapButton(bookmarkButton);
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'http://example.com/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
#### I'm h4
|
||||
##### I'm h5
|
||||
###### I'm h6''';
|
||||
|
||||
testWidgets('paste markdowns', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
expect(text, 'I\'m h$i');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste markdowns as plain', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
pasteAsPlain: true,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
final expectText = '${'#' * i} I\'m h$i';
|
||||
expect(text, expectText);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
Future<void> pasteContent(
|
||||
void Function(EditorState editorState) test, {
|
||||
FutureOr<void> Function(EditorState editorState) test, {
|
||||
Future<void> Function(EditorState editorState)? beforeTest,
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
bool pasteAsPlain = false,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
await tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await createNewPageWithNameUnderParent(name: 'Test Document');
|
||||
await createNewPageWithNameUnderParent();
|
||||
// tap the editor
|
||||
await tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
@@ -502,10 +557,11 @@ extension on WidgetTester {
|
||||
await simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
await pumpAndSettle(const Duration(milliseconds: 1000));
|
||||
|
||||
test(editor.getCurrentEditorState());
|
||||
await test(editor.getCurrentEditorState());
|
||||
}
|
||||
}
|
||||
+2
@@ -13,6 +13,8 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final finder = find.text(gettingStarted, findRichText: true);
|
||||
await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));
|
||||
|
||||
// create a new document
|
||||
const pageName = 'Test Document';
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
import 'document_inline_page_reference_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Document deletion', () {
|
||||
testWidgets('Trash breadcrumb', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// This test shares behavior with the inline page reference test, thus
|
||||
// we utilize the same helper functions there.
|
||||
final name = await createDocumentToReference(tester);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await triggerReferenceDocumentBySlashMenu(tester);
|
||||
|
||||
// Search for prefix of document
|
||||
await enterDocumentText(tester);
|
||||
|
||||
// Select result
|
||||
final optionFinder = find.descendant(
|
||||
of: find.byType(InlineActionsHandler),
|
||||
matching: find.text(name),
|
||||
);
|
||||
|
||||
await tester.tap(optionFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
expect(mentionBlock, findsOneWidget);
|
||||
|
||||
// Delete the page
|
||||
await tester.hoverOnPageName(
|
||||
name,
|
||||
onHover: () async => tester.tapDeletePageButton(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to the deleted page from the inline mention
|
||||
await tester.tap(mentionBlock);
|
||||
await tester.pumpUntilFound(find.byType(TrashBreadcrumb));
|
||||
|
||||
expect(find.byType(TrashBreadcrumb), findsOneWidget);
|
||||
|
||||
// Navigate using the trash breadcrumb
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TrashBreadcrumb),
|
||||
matching: find.text(
|
||||
LocaleKeys.trash_text.tr(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr()));
|
||||
|
||||
// Restore all
|
||||
await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.trash_restore.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate back to the document
|
||||
await tester.openPage('Getting started');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(mentionBlock);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TrashBreadcrumb), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
String generateRandomString(int len) {
|
||||
final r = Random();
|
||||
return String.fromCharCodes(
|
||||
List.generate(len, (index) => r.nextInt(33) + 89),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'document find menu test',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
// tap editor to get focus
|
||||
await tester.tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
// set clipboard data
|
||||
final data = [
|
||||
"123456\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"1234567\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"12345678\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
].join();
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: data,
|
||||
),
|
||||
);
|
||||
|
||||
// paste
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// go back to beginning of document
|
||||
// FIXME: Cannot run Ctrl+F unless selection is on screen
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
|
||||
|
||||
// press cmd/ctrl+F to display the find menu
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyF,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);
|
||||
|
||||
final textField = find.descendant(
|
||||
of: find.byType(FindAndReplaceMenuWidget),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
textField,
|
||||
"123456",
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("123456", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("1234567", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.showKeyboard(textField);
|
||||
await tester.idle();
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("12345678", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// tap next button, go back to beginning of document
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(FindMenu),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_down_s),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("123456", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// press cmd/ctrl+F to display the find menu
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyF,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);
|
||||
|
||||
/// press esc to dismiss the find menu
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
const _firstDocName = "Inline Sub Page Mention";
|
||||
const _createdPageName = "hi world";
|
||||
|
||||
// Test cases that are covered in this file:
|
||||
// - [x] Insert sub page mention from action menu (+)
|
||||
// - [x] Delete sub page mention from editor
|
||||
// - [x] Delete page from sidebar
|
||||
// - [x] Delete page from sidebar and then trash
|
||||
// - [x] Undo delete sub page mention
|
||||
// - [x] Cut+paste in same document
|
||||
// - [x] Cut+paste in different document
|
||||
// - [x] Cut+paste in same document and then paste again in same document
|
||||
// - [x] Turn paragraph with sub page mention into a heading
|
||||
// - [x] Turn heading with sub page mention into a paragraph
|
||||
// - [x] Duplicate a Block containing two sub page mentions
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document inline sub-page mention tests:', () {
|
||||
testWidgets('Insert (& delete) a sub page mention from action menu',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.insertInlineSubPageFromPlusMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: _firstDocName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Delete from editor
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNothing);
|
||||
expect(find.byType(MentionSubPageBlock), findsNothing);
|
||||
|
||||
// Undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Move to trash (delete from sidebar)
|
||||
await tester.rightClickOnPageName(_createdPageName);
|
||||
await tester.tapButtonWithName(ViewMoreActionType.delete.name);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsOneWidget);
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.document_mention_trashHint.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Delete from trash
|
||||
await tester.tapTrashButton();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.openPage(_firstDocName);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNothing);
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.document_mention_deletedPage.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Cut+paste in same document and cut+paste in different document',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.insertInlineSubPageFromPlusMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: _firstDocName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Cut from editor
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNothing);
|
||||
expect(find.byType(MentionSubPageBlock), findsNothing);
|
||||
|
||||
// Paste in same document
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Cut again
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Create another document
|
||||
const anotherDocName = "Another Document";
|
||||
await tester.createOpenRenameDocumentUnderParent(
|
||||
name: anotherDocName,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNothing);
|
||||
expect(find.byType(MentionSubPageBlock), findsNothing);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Paste in document
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpUntilFound(find.byType(MentionSubPageBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsOneWidget);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: anotherDocName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
});
|
||||
testWidgets(
|
||||
'Cut+paste in same docuemnt and then paste again in same document',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.insertInlineSubPageFromPlusMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: _firstDocName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Cut from editor
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNothing);
|
||||
expect(find.byType(MentionSubPageBlock), findsNothing);
|
||||
|
||||
// Paste in same document
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Paste again
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
|
||||
expect(find.text('$_createdPageName (copy)'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Turn into w/ sub page mentions', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.insertInlineSubPageFromPlusMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: _firstDocName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
final headingText = LocaleKeys.document_slashMenu_name_heading1.tr();
|
||||
final paragraphText = LocaleKeys.document_slashMenu_name_text.tr();
|
||||
|
||||
// Turn into heading
|
||||
await tester.editor.openTurnIntoMenu([0]);
|
||||
await tester.tapButton(find.findTextInFlowyText(headingText));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
|
||||
// Turn into paragraph
|
||||
await tester.editor.openTurnIntoMenu([0]);
|
||||
await tester.tapButton(find.findTextInFlowyText(paragraphText));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsNWidgets(2));
|
||||
expect(find.byType(MentionSubPageBlock), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Duplicate a block containing two sub page mentions',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.insertInlineSubPageFromPlusMenu();
|
||||
|
||||
// Copy paste it
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsOneWidget);
|
||||
expect(find.text("$_createdPageName (copy)"), findsOneWidget);
|
||||
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
|
||||
|
||||
// Duplicate node from block action menu
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(_createdPageName), findsOneWidget);
|
||||
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
|
||||
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Cancel inline page reference menu by space', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showPlusMenu();
|
||||
|
||||
// Cancel by space
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.space,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(InlineActionsMenu), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension _InlineSubPageTestHelper on WidgetTester {
|
||||
Future<void> insertInlineSubPageFromPlusMenu() async {
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
|
||||
await editor.showPlusMenu();
|
||||
|
||||
// Workaround to allow typing a document name
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
tester: this,
|
||||
withKeyUp: true,
|
||||
[
|
||||
LogicalKeyboardKey.keyH,
|
||||
LogicalKeyboardKey.keyI,
|
||||
LogicalKeyboardKey.space,
|
||||
LogicalKeyboardKey.keyW,
|
||||
LogicalKeyboardKey.keyO,
|
||||
LogicalKeyboardKey.keyR,
|
||||
LogicalKeyboardKey.keyL,
|
||||
LogicalKeyboardKey.keyD,
|
||||
],
|
||||
);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
tester: this,
|
||||
withKeyUp: true,
|
||||
[LogicalKeyboardKey.enter],
|
||||
);
|
||||
await pumpUntilFound(find.byType(MentionSubPageBlock));
|
||||
}
|
||||
}
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const avaliableLink = 'https://appflowy.io/',
|
||||
unavailableLink = 'www.thereIsNoting.com';
|
||||
|
||||
Future<void> preparePage(WidgetTester tester, {String? pageName}) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: pageName);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
}
|
||||
|
||||
Future<void> pasteLink(WidgetTester tester, String link) async {
|
||||
await getIt<ClipboardService>()
|
||||
.setData(ClipboardServiceData(plainText: link));
|
||||
|
||||
/// paste the link
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> pasteAs(
|
||||
WidgetTester tester,
|
||||
String link,
|
||||
PasteMenuType type, {
|
||||
Duration waitTime = const Duration(milliseconds: 500),
|
||||
}) async {
|
||||
await pasteLink(tester, link);
|
||||
final convertToMentionButton = find.text(type.title);
|
||||
await tester.tapButton(convertToMentionButton);
|
||||
await tester.pumpAndSettle(waitTime);
|
||||
}
|
||||
|
||||
void checkUrl(Node node, String link) {
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': link,
|
||||
'attributes': {'href': link},
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
void checkMention(Node node, String link) {
|
||||
final delta = node.delta!;
|
||||
final insert = (delta.first as TextInsert).text;
|
||||
final attributes = delta.first.attributes;
|
||||
expect(insert, MentionBlockKeys.mentionChar);
|
||||
final mention =
|
||||
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
|
||||
expect(mention[MentionBlockKeys.type], MentionType.externalLink.name);
|
||||
expect(mention[MentionBlockKeys.url], avaliableLink);
|
||||
}
|
||||
|
||||
void checkBookmark(Node node, String link) {
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], link);
|
||||
}
|
||||
|
||||
void checkEmbed(Node node, String link) {
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], link);
|
||||
}
|
||||
|
||||
group('Paste as URL', () {
|
||||
Future<void> pasteAndTurnInto(
|
||||
WidgetTester tester,
|
||||
String link,
|
||||
String title,
|
||||
) async {
|
||||
await pasteLink(tester, link);
|
||||
final convertToLinkButton = find
|
||||
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
|
||||
/// hover link and turn into mention
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(LinkHoverTrigger),
|
||||
onHover: () async {
|
||||
final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(turnintoButton);
|
||||
final convertToButton = find.text(title);
|
||||
await tester.tapButton(convertToButton);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteLink(tester, link);
|
||||
final convertToLinkButton = find
|
||||
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into mention', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toMention.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into bookmark', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toBookmark.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into embed', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toEmbed.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
});
|
||||
|
||||
group('Paste as Mention', () {
|
||||
Future<void> pasteAsMention(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.mention);
|
||||
|
||||
String getMentionLink(Node node) {
|
||||
final insert = node.delta?.first as TextInsert?;
|
||||
final mention = insert?.attributes?[MentionBlockKeys.mention]
|
||||
as Map<String, dynamic>?;
|
||||
return mention?[MentionBlockKeys.url] ?? '';
|
||||
}
|
||||
|
||||
Future<void> hoverMentionAndClick(
|
||||
WidgetTester tester,
|
||||
String command,
|
||||
) async {
|
||||
final mentionLink = find.byType(MentionLinkBlock);
|
||||
expect(mentionLink, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
mentionLink,
|
||||
onHover: () async {
|
||||
final errorPreview = find.byType(MentionLinkErrorPreview);
|
||||
expect(errorPreview, findsOneWidget);
|
||||
final convertButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(convertButton);
|
||||
final menuButton = find.text(command);
|
||||
await tester.tapButton(menuButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as mention', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as mention and copy link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
final mentionLink = find.byType(MentionLinkBlock);
|
||||
expect(mentionLink, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
mentionLink,
|
||||
onHover: () async {
|
||||
final preview = find.byType(MentionLinkPreview);
|
||||
if (!preview.hasFound) {
|
||||
final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(copyButton);
|
||||
} else {
|
||||
final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
final copyButton =
|
||||
find.text(MentionLinktMenuCommand.copyLink.title);
|
||||
await tester.tapButton(copyButton);
|
||||
}
|
||||
},
|
||||
);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
expect(clipboardContent.plainText, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto url', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toURL.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto embed', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toEmbed.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto bookmark', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toBookmark.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and remove link', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.removeLink.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{'insert': link},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('Paste as Bookmark', () {
|
||||
Future<void> pasteAsBookmark(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.bookmark);
|
||||
|
||||
Future<void> hoverAndClick(
|
||||
WidgetTester tester,
|
||||
LinkPreviewMenuCommand command,
|
||||
) async {
|
||||
final bookmark = find.byType(CustomLinkPreviewBlockComponent);
|
||||
expect(bookmark, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
bookmark,
|
||||
onHover: () async {
|
||||
final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
|
||||
await tester.tapButton(menuButton);
|
||||
final commandButton = find.text(command.title);
|
||||
await tester.tapButton(commandButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as bookmark', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to mention',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to url', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to embed',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and copy link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
expect(clipboardContent.plainText, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and replace link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.replace);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
|
||||
await tester.enterText(find.byType(TextFormField), unavailableLink);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_replace.tr()));
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, unavailableLink);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and remove link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{'insert': link},
|
||||
]);
|
||||
});
|
||||
});
|
||||
group('Paste as Embed', () {
|
||||
Future<void> pasteAsEmbed(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.embed);
|
||||
|
||||
Future<void> hoverAndConvert(
|
||||
WidgetTester tester,
|
||||
LinkEmbedConvertCommand command,
|
||||
) async {
|
||||
final embed = find.byType(LinkEmbedBlockComponent);
|
||||
expect(embed, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
embed,
|
||||
onHover: () async {
|
||||
final menuButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(menuButton);
|
||||
final commandButton = find.text(command.title);
|
||||
await tester.tapButton(commandButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as embed', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to mention',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to url', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to bookmark',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
});
|
||||
}
|
||||
+106
@@ -1,4 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -31,4 +37,104 @@ void main() {
|
||||
expect(pageFinder, findsNWidgets(1));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('count title towards word count', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
Finder title = tester.editor.findDocumentTitle('');
|
||||
|
||||
await tester.openMoreViewActions();
|
||||
final viewMetaInfo = find.byType(ViewMetaInfo);
|
||||
expect(viewMetaInfo, findsOneWidget);
|
||||
|
||||
ViewMetaInfo viewMetaInfoWidget =
|
||||
viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
Counters titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
|
||||
expect(titleCounter.charCount, 0);
|
||||
expect(titleCounter.wordCount, 0);
|
||||
|
||||
/// input [str1] within title
|
||||
const str1 = 'Hello',
|
||||
str2 = '$str1 AppFlowy',
|
||||
str3 = '$str2!',
|
||||
str4 = 'Hello world';
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str1);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str1.length);
|
||||
expect(titleCounter.wordCount, 1);
|
||||
|
||||
/// input [str2] within title
|
||||
title = tester.editor.findDocumentTitle(str1);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str2);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str2.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str3] within title
|
||||
title = tester.editor.findDocumentTitle(str2);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str3);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str3.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str4] within document
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor
|
||||
.getCurrentEditorState()
|
||||
.insertTextAtCurrentSelection(str4);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
final texts =
|
||||
find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));
|
||||
expect(texts, findsNWidgets(3));
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
final Counters documentCounters = viewMetaInfoWidget.documentCounters!;
|
||||
final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,
|
||||
charCounter = texts.evaluate().elementAt(1).widget as FlowyText;
|
||||
final numberFormat = NumberFormat();
|
||||
expect(
|
||||
wordCounter.text,
|
||||
LocaleKeys.moreAction_wordCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(titleCounter.wordCount + documentCounters.wordCount)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
charCounter.text,
|
||||
LocaleKeys.moreAction_charCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(
|
||||
titleCounter.charCount + documentCounters.charCount,
|
||||
)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
+137
-3
@@ -1,3 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -7,9 +12,23 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// +, ... button beside the block component.
|
||||
group('document with option action button', () {
|
||||
testWidgets(
|
||||
'click + to add a block after current selection, and click + and option key to add a block before current selection',
|
||||
group('block option action:', () {
|
||||
Future<void> turnIntoBlock(
|
||||
WidgetTester tester,
|
||||
Path path, {
|
||||
required String menuText,
|
||||
required String afterType,
|
||||
}) async {
|
||||
await tester.editor.openTurnIntoMenu(path);
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(menuText),
|
||||
);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath(path);
|
||||
expect(node?.type, afterType);
|
||||
}
|
||||
|
||||
testWidgets('''click + to add a block after current selection,
|
||||
and click + and option key to add a block before current selection''',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -40,5 +59,120 @@ void main() {
|
||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
|
||||
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('turn into - single line', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
const name = 'Test Document';
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
await tester.openPage(name);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('turn into');
|
||||
|
||||
// click the block option button to convert it to another blocks
|
||||
final values = {
|
||||
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.editor_bulletedListShortForm.tr():
|
||||
BulletedListBlockKeys.type,
|
||||
LocaleKeys.editor_numberedListShortForm.tr():
|
||||
NumberedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
|
||||
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
|
||||
};
|
||||
|
||||
for (final value in values.entries) {
|
||||
final menuText = value.key;
|
||||
final afterType = value.value;
|
||||
await turnIntoBlock(
|
||||
tester,
|
||||
[0],
|
||||
menuText: menuText,
|
||||
afterType: afterType,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('turn into - multi lines', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
const name = 'Test Document';
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
await tester.openPage(name);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('turn into 1');
|
||||
await tester.ime.insertCharacter('\n');
|
||||
await tester.ime.insertText('turn into 2');
|
||||
|
||||
// click the block option button to convert it to another blocks
|
||||
final values = {
|
||||
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.editor_bulletedListShortForm.tr():
|
||||
BulletedListBlockKeys.type,
|
||||
LocaleKeys.editor_numberedListShortForm.tr():
|
||||
NumberedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
|
||||
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
|
||||
};
|
||||
|
||||
for (final value in values.entries) {
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
editorState.selection = Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [1], offset: 2),
|
||||
);
|
||||
final menuText = value.key;
|
||||
final afterType = value.value;
|
||||
await turnIntoBlock(
|
||||
tester,
|
||||
[0],
|
||||
menuText: menuText,
|
||||
afterType: afterType,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'selecting the parent should deselect all the child nodes as well',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
const name = 'Test Document';
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
await tester.openPage(name);
|
||||
|
||||
// create a nested list
|
||||
// Item 1
|
||||
// Nested Item 1
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('Item 1');
|
||||
await tester.ime.insertCharacter('\n');
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.ime.insertText('Nested Item 1');
|
||||
|
||||
// select the 'Nested Item 1' and then tap the option button of the 'Item 1'
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0, 0], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
await tester.pumpAndSettle();
|
||||
expect(editorState.selection, selection);
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+37
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -47,5 +48,41 @@ void main() {
|
||||
|
||||
expect(editorState.selection!.start.offset, 0);
|
||||
});
|
||||
|
||||
testWidgets('select and delete text', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// input text
|
||||
final editor = tester.editor;
|
||||
final editorState = editor.getCurrentEditorState();
|
||||
|
||||
const inputText = 'Test for text selection and deletion';
|
||||
final texts = inputText.split(' ');
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(inputText);
|
||||
|
||||
/// selecte and delete
|
||||
int index = 0;
|
||||
while (texts.isNotEmpty) {
|
||||
final text = texts.removeAt(0);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: index),
|
||||
end: Position(path: [0], offset: index + text.length),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
|
||||
index++;
|
||||
}
|
||||
|
||||
/// excpete the text value is correct
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
final nodeText = node.delta?.toPlainText() ?? '';
|
||||
expect(nodeText, ' ' * (index - 1));
|
||||
});
|
||||
});
|
||||
}
|
||||
+137
-216
@@ -1,16 +1,19 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
// Test cases for the Document SubPageBlock that needs to be covered:
|
||||
@@ -26,13 +29,25 @@ import '../../shared/util.dart';
|
||||
// - [x] Redo adding a SubPageBlock (Expect the view to be restored)
|
||||
// - [x] Redo delete of a SubPageBlock (Expect the view to be moved to trash again)
|
||||
// - [x] Renaming a child view (Expect the view name to be updated in the document)
|
||||
// - [x] Deleting a view (to trash) linked to a SubPageBlock shows a hint that the view is in trash (Expect a hint to be shown)
|
||||
// - [x] Deleting a view (in trash) linked to a SubPageBlock deletes the SubPageBlock (Expect the SubPageBlock to be deleted)
|
||||
// - [x] Deleting a view (to trash) linked to a SubPageBlock deleted the SubPageBlock (Expect the SubPageBlock to be deleted)
|
||||
// - [x] Duplicating a SubPageBlock node from Action Menu (Expect a new view is created under current view with same content and name + (copy))
|
||||
// - [x] Dragging a SubPageBlock node to a new position in the document (Expect everything to be normal)
|
||||
|
||||
/// The defaut page name is empty, if we're looking for a "text" we can look for
|
||||
/// [LocaleKeys.menuAppHeader_defaultNewPageName] but it won't work for eg. hoverOnPageName
|
||||
/// as it looks at the text provided instead of the actual displayed text.
|
||||
///
|
||||
const _defaultPageName = "";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Document SubPageBlock tests', () {
|
||||
testWidgets('Insert a new SubPageBlock from Slash menu items',
|
||||
@@ -43,11 +58,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsNWidgets(3),
|
||||
@@ -62,16 +72,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
@@ -89,16 +90,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.hoverAndClickOptionAddButton([0], false);
|
||||
@@ -146,16 +138,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.hoverAndClickOptionAddButton([0], false);
|
||||
@@ -208,16 +191,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor
|
||||
@@ -253,16 +227,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor
|
||||
@@ -300,39 +265,6 @@ void main() {
|
||||
expect(find.text('Child page (copy)'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Undo adding a SubPageBlock', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// Undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsNothing);
|
||||
expect(find.text('Child page'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Undo delete of a SubPageBlock', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -340,16 +272,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
@@ -359,6 +282,11 @@ void main() {
|
||||
expect(find.text('Child page'), findsNothing);
|
||||
expect(find.byType(SubPageBlockComponent), findsNothing);
|
||||
|
||||
// Since there is no selection active in editor before deleting Node,
|
||||
// we need to give focus back to the editor
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
@@ -370,55 +298,6 @@ void main() {
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
});
|
||||
|
||||
// Redo: undoing adding a subpage block, then redoing to bring it back
|
||||
// -> Add a subpage block
|
||||
// -> Undo
|
||||
// -> Redo
|
||||
testWidgets('Redo adding of a SubPageBlock', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// Undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsNothing);
|
||||
expect(find.text('Child page'), findsNothing);
|
||||
|
||||
// Redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isShiftPressed: true,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
// Redo: undoing deleting a subpage block, then redoing to delete it again
|
||||
// -> Add a subpage block
|
||||
// -> Delete
|
||||
@@ -431,16 +310,7 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
// Delete
|
||||
@@ -476,51 +346,25 @@ void main() {
|
||||
expect(find.text('Child page'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Delete a view first to trash, then from trash',
|
||||
(tester) async {
|
||||
testWidgets('Delete a view from sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
|
||||
final hintText = LocaleKeys.document_plugins_subPage_inTrashHint.tr();
|
||||
expect(find.text(hintText), findsNothing);
|
||||
|
||||
await tester.hoverOnPageName('Child page');
|
||||
await tester.tapDeletePageButton();
|
||||
await tester.hoverOnPageName(
|
||||
'Child page',
|
||||
onHover: () async {
|
||||
await tester.tapDeletePageButton();
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text(hintText), findsOne);
|
||||
|
||||
// Go to trash
|
||||
await tester.tapTrashButton();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on delete all button
|
||||
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap ok to delete app pages in trash
|
||||
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.openPage('SubPageBlock');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Child page'), findsNothing);
|
||||
expect(find.byType(SubPageBlockComponent), findsNothing);
|
||||
});
|
||||
@@ -531,17 +375,7 @@ void main() {
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester
|
||||
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
|
||||
await tester.renamePage('Child page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
@@ -561,11 +395,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
|
||||
final beforeNode = tester.editor.getNodeAtPath([1]);
|
||||
@@ -578,10 +407,86 @@ void main() {
|
||||
expect(afterNode.type, SubPageBlockKeys.type);
|
||||
expect(afterNode.type, beforeNode.type);
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.document_plugins_subPage_inTrashHint.tr()),
|
||||
findsNothing,
|
||||
});
|
||||
|
||||
testWidgets('turn into page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
// Insert nested list
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode(
|
||||
[0],
|
||||
bulletedListNode(
|
||||
text: 'Parent',
|
||||
children: [
|
||||
bulletedListNode(text: 'Child 1'),
|
||||
bulletedListNode(text: 'Child 2'),
|
||||
],
|
||||
),
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsNothing);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text(LocaleKeys.editor_page.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(find.text('Parent'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Displaying icon of subpage', (tester) async {
|
||||
const firstPage = 'FirstPage';
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: firstPage);
|
||||
final icon = await tester.loadIcon();
|
||||
|
||||
/// create subpage
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_subPage_name.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
|
||||
/// add icon
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.tapIcon(icon);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.openPage(firstPage);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: firstPage,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
/// check if there is a icon in document
|
||||
final iconWidget = find.byWidgetPredicate((w) {
|
||||
if (w is! RawEmojiIconWidget) return false;
|
||||
final iconData = w.emoji.emoji;
|
||||
return iconData == icon.emoji;
|
||||
});
|
||||
expect(iconWidget, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -602,6 +507,22 @@ extension _SubPageTestHelper on WidgetTester {
|
||||
offset: 100,
|
||||
);
|
||||
|
||||
// Navigate to the previous page to see the SubPageBlock
|
||||
await openPage('SubPageBlock');
|
||||
await pumpAndSettle();
|
||||
|
||||
await pumpUntilFound(find.byType(SubPageBlockComponent));
|
||||
}
|
||||
|
||||
Future<void> renamePageWithSecondary(
|
||||
String currentName,
|
||||
String newName,
|
||||
) async {
|
||||
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
|
||||
await rightClickOnPageName(currentName);
|
||||
await tapButtonWithName(ViewMoreActionType.rename.name);
|
||||
await enterText(find.byType(TextFormField), newName);
|
||||
await tapOKButton();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_alignment_test.dart' as document_alignment_test;
|
||||
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_inline_page_reference_test.dart'
|
||||
as document_inline_page_reference_test;
|
||||
import 'document_more_actions_test.dart' as document_more_actions_test;
|
||||
import 'document_option_action_test.dart' as document_option_action_test;
|
||||
import 'document_shortcuts_test.dart' as document_shortcuts_test;
|
||||
import 'document_text_direction_test.dart' as document_text_direction_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_file_test.dart' as document_with_file_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'document_with_multi_image_block_test.dart'
|
||||
as document_with_multi_image_block_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_create_and_delete_test.main();
|
||||
document_edit_test.main();
|
||||
document_with_database_test.main();
|
||||
document_with_inline_page_test.main();
|
||||
document_with_inline_math_equation_test.main();
|
||||
document_with_cover_image_test.main();
|
||||
document_with_outline_block.main();
|
||||
document_with_toggle_list_test.main();
|
||||
document_copy_and_paste_test.main();
|
||||
document_codeblock_paste_test.main();
|
||||
document_alignment_test.main();
|
||||
document_text_direction_test.main();
|
||||
document_option_action_test.main();
|
||||
document_with_image_block_test.main();
|
||||
document_with_multi_image_block_test.main();
|
||||
document_inline_page_reference_test.main();
|
||||
document_more_actions_test.main();
|
||||
document_with_file_test.main();
|
||||
document_shortcuts_test.main();
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_create_and_delete_test.main();
|
||||
document_edit_test.main();
|
||||
document_with_database_test.main();
|
||||
document_with_inline_page_test.main();
|
||||
document_with_inline_math_equation_test.main();
|
||||
document_with_cover_image_test.main();
|
||||
// Don't add new tests here.
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;
|
||||
import 'document_deletion_test.dart' as document_deletion_test;
|
||||
import 'document_inline_sub_page_test.dart' as document_inline_sub_page_test;
|
||||
import 'document_option_action_test.dart' as document_option_action_test;
|
||||
import 'document_title_test.dart' as document_title_test;
|
||||
import 'document_with_date_reminder_test.dart'
|
||||
as document_with_date_reminder_test;
|
||||
import 'document_with_toggle_heading_block_test.dart'
|
||||
as document_with_toggle_heading_block_test;
|
||||
import 'document_sub_page_test.dart' as document_sub_page_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_title_test.main();
|
||||
document_app_lifecycle_test.main();
|
||||
document_with_date_reminder_test.main();
|
||||
document_deletion_test.main();
|
||||
document_option_action_test.main();
|
||||
document_inline_sub_page_test.main();
|
||||
document_with_toggle_heading_block_test.main();
|
||||
document_sub_page_test.main();
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_alignment_test.dart' as document_alignment_test;
|
||||
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
import 'document_text_direction_test.dart' as document_text_direction_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_with_outline_block.main();
|
||||
document_with_toggle_list_test.main();
|
||||
document_copy_and_paste_test.main();
|
||||
document_codeblock_paste_test.main();
|
||||
document_alignment_test.main();
|
||||
document_text_direction_test.main();
|
||||
|
||||
// Don't add new tests here.
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_block_option_test.dart' as document_block_option_test;
|
||||
import 'document_find_menu_test.dart' as document_find_menu_test;
|
||||
import 'document_inline_page_reference_test.dart'
|
||||
as document_inline_page_reference_test;
|
||||
import 'document_more_actions_test.dart' as document_more_actions_test;
|
||||
import 'document_shortcuts_test.dart' as document_shortcuts_test;
|
||||
import 'document_toolbar_test.dart' as document_toolbar_test;
|
||||
import 'document_with_file_test.dart' as document_with_file_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_multi_image_block_test.dart'
|
||||
as document_with_multi_image_block_test;
|
||||
import 'document_with_simple_table_test.dart'
|
||||
as document_with_simple_table_test;
|
||||
import 'document_link_preview_test.dart' as document_link_preview_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_with_image_block_test.main();
|
||||
document_with_multi_image_block_test.main();
|
||||
document_inline_page_reference_test.main();
|
||||
document_more_actions_test.main();
|
||||
document_with_file_test.main();
|
||||
document_shortcuts_test.main();
|
||||
document_block_option_test.main();
|
||||
document_find_menu_test.main();
|
||||
document_toolbar_test.main();
|
||||
document_with_simple_table_test.main();
|
||||
document_link_preview_test.main();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -221,6 +222,85 @@ void main() {
|
||||
expect(newTitle, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('execute undo and redo in title', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
final title = tester.editor.findDocumentTitle('');
|
||||
await tester.enterText(title, _testDocumentName);
|
||||
// press a random key to make the undo stack not empty
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
// wait for the undo to be applied
|
||||
await tester.pumpAndSettle(Durations.long1);
|
||||
|
||||
// expect the title is empty
|
||||
expect(
|
||||
tester
|
||||
.widget<TextField>(
|
||||
tester.editor.findDocumentTitle(''),
|
||||
)
|
||||
.controller
|
||||
?.text,
|
||||
'',
|
||||
);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(Durations.short1);
|
||||
|
||||
if (UniversalPlatform.isMacOS) {
|
||||
expect(
|
||||
tester
|
||||
.widget<TextField>(
|
||||
tester.editor.findDocumentTitle(_testDocumentName),
|
||||
)
|
||||
.controller
|
||||
?.text,
|
||||
_testDocumentName,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('escape key should exit the editing mode', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
final title = tester.editor.findDocumentTitle('');
|
||||
await tester.enterText(title, _testDocumentName);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
tester
|
||||
.widget<TextField>(
|
||||
tester.editor.findDocumentTitle(_testDocumentName),
|
||||
)
|
||||
.focusNode
|
||||
?.hasFocus,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('press arrow down key in title, check if the cursor flashes',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
@@ -248,5 +328,46 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'hover on the cover title, check if the add icon & add cover button are shown',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
final title = tester.editor.findDocumentTitle('');
|
||||
await tester.hoverOnWidget(
|
||||
title,
|
||||
onHover: () async {
|
||||
expect(find.byType(DocumentCoverWidget), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('paste text in title, check if the text is updated',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
await Clipboard.setData(const ClipboardData(text: _testDocumentName));
|
||||
|
||||
final title = tester.editor.findDocumentTitle('');
|
||||
await tester.tapButton(title);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final newTitle = tester.editor.findDocumentTitle(_testDocumentName);
|
||||
expect(newTitle, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> selectText(WidgetTester tester, String text) async {
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> prepareForToolbar(WidgetTester tester, String text) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await selectText(tester, text);
|
||||
}
|
||||
|
||||
group('document toolbar:', () {
|
||||
testWidgets('font family', (tester) async {
|
||||
await prepareForToolbar(tester, 'font family');
|
||||
|
||||
// tap more options button
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m);
|
||||
// tap the font family button
|
||||
final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey);
|
||||
await tester.tapButton(fontFamilyButton);
|
||||
|
||||
// expect to see the font family dropdown immediately
|
||||
expect(find.byType(FontFamilyDropDown), findsOneWidget);
|
||||
|
||||
// click the font family 'Abel'
|
||||
const abel = 'Abel';
|
||||
await tester.tapButton(find.text(abel));
|
||||
|
||||
// check the text is updated to 'Abel'
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
expect(
|
||||
editorState.getDeltaAttributeValueInSelection(
|
||||
AppFlowyRichTextKeys.fontFamily,
|
||||
),
|
||||
abel,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('heading 1~3', (tester) async {
|
||||
const text = 'heading';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
Future<void> testChangeHeading(
|
||||
FlowySvgData svg,
|
||||
String title,
|
||||
int level,
|
||||
) async {
|
||||
/// tap suggestions item
|
||||
final suggestionsButton = find.byKey(kSuggestionsItemKey);
|
||||
await tester.tapButton(suggestionsButton);
|
||||
|
||||
/// tap item
|
||||
await tester.ensureVisible(find.byFlowySvg(svg));
|
||||
await tester.tapButton(find.byFlowySvg(svg));
|
||||
|
||||
/// check the type of node is [HeadingBlockKeys.type]
|
||||
await selectText(tester, text);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = editorState.selection!;
|
||||
final node = editorState.getNodeAtPath(selection.start.path)!,
|
||||
nodeLevel = node.attributes[HeadingBlockKeys.level]!;
|
||||
expect(node.type, HeadingBlockKeys.type);
|
||||
expect(nodeLevel, level);
|
||||
|
||||
/// show toolbar again
|
||||
await selectText(tester, text);
|
||||
|
||||
/// the text of suggestions item should be changed
|
||||
expect(
|
||||
find.descendant(of: suggestionsButton, matching: find.text(title)),
|
||||
findsOneWidget,
|
||||
);
|
||||
}
|
||||
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h1_m,
|
||||
LocaleKeys.document_toolbar_h1.tr(),
|
||||
1,
|
||||
);
|
||||
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h2_m,
|
||||
LocaleKeys.document_toolbar_h2.tr(),
|
||||
2,
|
||||
);
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h3_m,
|
||||
LocaleKeys.document_toolbar_h3.tr(),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('toggle 1~3', (tester) async {
|
||||
const text = 'toggle';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
Future<void> testChangeToggle(
|
||||
FlowySvgData svg,
|
||||
String title,
|
||||
int? level,
|
||||
) async {
|
||||
/// tap suggestions item
|
||||
final suggestionsButton = find.byKey(kSuggestionsItemKey);
|
||||
await tester.tapButton(suggestionsButton);
|
||||
|
||||
/// tap item
|
||||
await tester.ensureVisible(find.byFlowySvg(svg));
|
||||
await tester.tapButton(find.byFlowySvg(svg));
|
||||
|
||||
/// check the type of node is [HeadingBlockKeys.type]
|
||||
await selectText(tester, text);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = editorState.selection!;
|
||||
final node = editorState.getNodeAtPath(selection.start.path)!,
|
||||
nodeLevel = node.attributes[ToggleListBlockKeys.level];
|
||||
expect(node.type, ToggleListBlockKeys.type);
|
||||
expect(nodeLevel, level);
|
||||
|
||||
/// show toolbar again
|
||||
await selectText(tester, text);
|
||||
|
||||
/// the text of suggestions item should be changed
|
||||
expect(
|
||||
find.descendant(of: suggestionsButton, matching: find.text(title)),
|
||||
findsOneWidget,
|
||||
);
|
||||
}
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_list_m,
|
||||
LocaleKeys.editor_toggleListShortForm.tr(),
|
||||
null,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h1_m,
|
||||
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
|
||||
1,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h2_m,
|
||||
LocaleKeys.editor_toggleHeading2ShortForm.tr(),
|
||||
2,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h3_m,
|
||||
LocaleKeys.editor_toggleHeading3ShortForm.tr(),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('toolbar will not rebuild after click item', (tester) async {
|
||||
const text = 'Test rebuilding';
|
||||
await prepareForToolbar(tester, text);
|
||||
Finder toolbar = find.byType(DesktopFloatingToolbar);
|
||||
Element toolbarElement = toolbar.evaluate().first;
|
||||
final elementHashcode = toolbarElement.hashCode;
|
||||
final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m),
|
||||
underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m),
|
||||
italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m);
|
||||
|
||||
/// tap format buttons
|
||||
await tester.tapButton(boldButton);
|
||||
await tester.tapButton(underlineButton);
|
||||
await tester.tapButton(italicButton);
|
||||
toolbar = find.byType(DesktopFloatingToolbar);
|
||||
toolbarElement = toolbar.evaluate().first;
|
||||
|
||||
/// check if the toolbar is not rebuilt
|
||||
expect(elementHashcode, toolbarElement.hashCode);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// check text formats
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('document toolbar: link', () {
|
||||
String? getLinkFromNode(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
final link = insert.attributes?.href;
|
||||
if (link != null) return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isPageLink(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
final isPage = insert.attributes?.isPage;
|
||||
if (isPage == true) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String getNodeText(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
if (insert is TextInsert) return insert.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
testWidgets('insert link and remove link', (tester) async {
|
||||
const text = 'insert link', link = 'https://test.appflowy.cloud';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
final toolbar = find.byType(DesktopFloatingToolbar);
|
||||
expect(toolbar, findsOneWidget);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
final createLinkMenu = find.byType(LinkCreateMenu);
|
||||
expect(createLinkMenu, findsOneWidget);
|
||||
|
||||
/// test esc to close
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
expect(toolbar, findsNothing);
|
||||
|
||||
/// show toolbar again
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await selectText(tester, text);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// insert link
|
||||
final textField = find.descendant(
|
||||
of: createLinkMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), link);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
final hoverMenu = find.byType(LinkHoverMenu);
|
||||
expect(hoverMenu, findsOneWidget);
|
||||
|
||||
/// copy link
|
||||
final copyButton = find.descendant(
|
||||
of: hoverMenu,
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_m),
|
||||
);
|
||||
await tester.tapButton(copyButton);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
final plainText = clipboardContent.plainText;
|
||||
expect(plainText, link);
|
||||
|
||||
/// remove link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), null);
|
||||
});
|
||||
|
||||
testWidgets('insert link and edit link', (tester) async {
|
||||
const text = 'edit link',
|
||||
link = 'https://test.appflowy.cloud',
|
||||
afterText = '$text after';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// search for page and select it
|
||||
final textField = find.descendant(
|
||||
of: find.byType(LinkCreateMenu),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, gettingStarted);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(isPageLink(node), true);
|
||||
expect(getLinkFromNode(node) == link, false);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
|
||||
/// click edit button to show LinkEditMenu
|
||||
final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);
|
||||
await tester.tapButton(editButton);
|
||||
final linkEditMenu = find.byType(LinkEditMenu);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// change the link text
|
||||
final titleField = find.descendant(
|
||||
of: linkEditMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(titleField, afterText);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)),
|
||||
);
|
||||
final linkField = find.ancestor(
|
||||
of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
/// apply the change
|
||||
final applyButton =
|
||||
find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr());
|
||||
await tester.tapButton(applyButton);
|
||||
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(isPageLink(node), false);
|
||||
expect(getLinkFromNode(node), link);
|
||||
expect(getNodeText(node), afterText);
|
||||
});
|
||||
});
|
||||
}
|
||||
+73
-4
@@ -1,20 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
group('cover image', () {
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('cover image:', () {
|
||||
testWidgets('document cover tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -52,6 +68,59 @@ void main() {
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
});
|
||||
|
||||
testWidgets('document cover local image tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
|
||||
// Insert a document cover
|
||||
await tester.editor.tapOnAddCover();
|
||||
tester.expectToSeeDocumentCover(CoverType.asset);
|
||||
|
||||
// Hover over the cover to show the 'Change Cover' and delete buttons
|
||||
await tester.editor.hoverOnCover();
|
||||
tester.expectChangeCoverAndDeleteButton();
|
||||
|
||||
// Change cover to a local image image
|
||||
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final imageFile = File(localImagePath)
|
||||
..writeAsBytesSync(imagePath.buffer.asUint8List());
|
||||
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnChangeCover();
|
||||
|
||||
final uploadButton = find.findTextInFlowyText(
|
||||
LocaleKeys.document_imageBlock_upload_label.tr(),
|
||||
);
|
||||
await tester.tapButton(uploadButton);
|
||||
|
||||
mockPickFilePaths(paths: [localImagePath]);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeDocumentCover(CoverType.file);
|
||||
|
||||
// Remove the cover
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnRemoveCover();
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Test if deleteImageFromLocalStorage(localImagePath) function is called once
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
expect(deleteImageTestCounter, 1);
|
||||
|
||||
// delete temp files
|
||||
await imageFile.delete();
|
||||
});
|
||||
|
||||
testWidgets('document icon tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -148,7 +217,7 @@ void main() {
|
||||
tester.expectViewHasIcon(
|
||||
gettingStarted,
|
||||
ViewLayoutPB.Document,
|
||||
punch,
|
||||
EmojiIconData.emoji(punch),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+104
@@ -1,12 +1,15 @@
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -174,9 +177,110 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced grid with many rows (load more option)',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(GridPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
|
||||
// test: the selection of editor should be clear when editing the grid
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(
|
||||
Position(path: [1]),
|
||||
),
|
||||
);
|
||||
final gridTextCell = find.byType(EditableTextCell).first;
|
||||
await tester.tapButton(gridTextCell);
|
||||
|
||||
expect(tester.editor.getCurrentEditorState().selection, isNull);
|
||||
|
||||
final editorScrollable = find
|
||||
.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is Scrollable && w.axis == Axis.vertical,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
|
||||
// Add 100 Rows to the linked database
|
||||
final addRowFinder = find.byType(GridAddRowButton);
|
||||
for (var i = 0; i < 100; i++) {
|
||||
await tester.scrollUntilVisible(
|
||||
addRowFinder,
|
||||
100,
|
||||
scrollable: editorScrollable,
|
||||
);
|
||||
await tester.tapButton(addRowFinder);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Since all rows visible are those we added, we should see all of them
|
||||
expect(find.byType(GridRow), findsNWidgets(103));
|
||||
|
||||
// Navigate to getting started
|
||||
await tester.openPage(gettingStarted);
|
||||
|
||||
// Navigate back to the document
|
||||
await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}');
|
||||
|
||||
// We see only 25 Grid Rows
|
||||
expect(find.byType(GridRow), findsNWidgets(25));
|
||||
|
||||
// We see Add row and load more button
|
||||
expect(find.byType(GridAddRowButton), findsOneWidget);
|
||||
expect(find.byType(GridRowLoadMoreButton), findsOneWidget);
|
||||
|
||||
// Load more rows, expect 50 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 50);
|
||||
|
||||
// Load more rows, expect 75 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 75);
|
||||
|
||||
// Load more rows, expect 100 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 100);
|
||||
|
||||
// Load more rows, expect 103 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 103);
|
||||
|
||||
// We no longer see load more option
|
||||
expect(find.byType(GridRowLoadMoreButton), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadMoreRows(
|
||||
WidgetTester tester,
|
||||
Finder scrollable, [
|
||||
int? expectedRows,
|
||||
]) async {
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(GridRowLoadMoreButton),
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(GridRowLoadMoreButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
if (expectedRows != null) {
|
||||
expect(find.byType(GridRow), findsNWidgets(expectedRows));
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a referenced database of [layout] into the document
|
||||
Future<void> insertLinkedDatabase(
|
||||
WidgetTester tester,
|
||||
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('date or reminder block in document:', () {
|
||||
testWidgets("insert date with time block", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'Date with time test',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final DateTime currentDateTime = DateTime.now();
|
||||
final String formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
|
||||
|
||||
// get current date in editor
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// tap on date field
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// tap the toggle of include time
|
||||
await tester.tap(find.byType(Toggle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add time 11:12
|
||||
final textField = find
|
||||
.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byType(TextField),
|
||||
)
|
||||
.last;
|
||||
await tester.pumpUntilFound(textField);
|
||||
await tester.enterText(textField, "11:12");
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// we will get field with current date and 11:12 as time
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate 11:12'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("insert date with reminder block", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'Date with reminder test',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final DateTime currentDateTime = DateTime.now();
|
||||
final String formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
|
||||
|
||||
// get current date in editor
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// tap on date field
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// tap reminder and set reminder to 1 day before
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// we will get field with current date reminder_clock.svg icon
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a date mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a date mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final DateTime currentDateTime = DateTime.now();
|
||||
final String formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
|
||||
|
||||
// get current date in editor
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("delete, undo and redo a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'delete, undo and redo a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and backspace to delete the mention
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
|
||||
// undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-3
@@ -47,9 +47,7 @@ void main() {
|
||||
mockPickFilePaths(paths: [filePath]);
|
||||
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
await tester.tap(
|
||||
find.text(LocaleKeys.document_plugins_file_fileUploadHint.tr()),
|
||||
);
|
||||
await tester.tapFileUploadHint();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FileUploadMenu), findsNothing);
|
||||
|
||||
-91
@@ -6,21 +6,17 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:run_with_network_images/run_with_network_images.dart';
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
@@ -80,93 +76,6 @@ void main() {
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('insert an image from network', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
const url =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], url);
|
||||
});
|
||||
|
||||
testWidgets('insert an image from unsplash', (tester) async {
|
||||
await runWithNetworkImages(() async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName('Unsplash');
|
||||
await tester.pumpUntilFound(find.byType(UnsplashImageWidget));
|
||||
expect(find.byType(UnsplashImageWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('insert two images from local file at once', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+124
-16
@@ -1,9 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -32,9 +34,15 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
final inlineMathEquationButton = find.text(
|
||||
LocaleKeys.document_toolbar_equation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
@@ -77,10 +85,15 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
var inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton =
|
||||
find.byFlowySvg(FlowySvgs.type_formula_m);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
@@ -92,17 +105,7 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: 1),
|
||||
);
|
||||
|
||||
// expect to the see the inline math equation button is highlighted
|
||||
inlineMathEquationButton = find.descendant(
|
||||
of: find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
),
|
||||
matching: find.byType(SVGIconItemWidget),
|
||||
);
|
||||
expect(
|
||||
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,
|
||||
isTrue,
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// cancel the format
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
@@ -113,5 +116,110 @@ void main() {
|
||||
|
||||
tester.expectToSeeText(formula);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline math equation and type something after it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'math equation',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton =
|
||||
find.byFlowySvg(FlowySvgs.type_formula_m);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
final inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
final inlineText = find.textContaining(text, findRichText: true);
|
||||
expect(inlineText, findsOneWidget);
|
||||
|
||||
// the text should be in the same line with the math equation
|
||||
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
|
||||
final textPosition = tester.getRect(inlineText);
|
||||
// allow 5px difference
|
||||
expect(
|
||||
(textPosition.top - inlineMathEquationPosition.top).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
expect(
|
||||
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert inline math equation by shortcut', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'insert inline math equation by shortcut',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// mock key event
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyE,
|
||||
isShiftPressed: true,
|
||||
isControlPressed: true,
|
||||
);
|
||||
|
||||
// expect to see the math equation block
|
||||
final inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
final inlineText = find.textContaining(text, findRichText: true);
|
||||
expect(inlineText, findsOneWidget);
|
||||
|
||||
// the text should be in the same line with the math equation
|
||||
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
|
||||
final textPosition = tester.getRect(inlineText);
|
||||
// allow 5px difference
|
||||
expect(
|
||||
(textPosition.top - inlineMathEquationPosition.top).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
expect(
|
||||
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -92,7 +92,21 @@ void main() {
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
await tester.tapButton(finder);
|
||||
expect(find.byType(AppFlowyErrorPage), findsOneWidget);
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page and type something after the page',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertInlinePage(tester, ViewLayoutPB.Grid);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
expect(find.textContaining(text, findRichText: true), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
@@ -26,7 +25,6 @@ import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
|
||||
Loaded 100 of 4650 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user