mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
chore(workspace): tighten quality gates (#2230)
This commit is contained in:
1 parent
e6e4c6f7b5
commit
5c2dca1c51
123 files changed
+4925
-5640
No files matched your search
@@ -15,9 +15,7 @@
|
||||
'Segoe UI Symbol', 'Segoe UI Historic', 'Apple Symbols',
|
||||
'Noto Sans Symbols', 'Noto Sans Symbols 2', 'Noto Sans Math', 'Cambria Math';
|
||||
--font-fallback-mono:
|
||||
'Noto Sans Mono',
|
||||
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR',
|
||||
var(--font-fallback-sans);
|
||||
'Noto Sans Mono', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', var(--font-fallback-sans);
|
||||
|
||||
--font-sans:
|
||||
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0980-0983, U+0985-098C, U+098F-0990, U+0993-09A8, U+09AA-09B0, U+09B2, U+09B6-09B9, U+09BC-09C4, U+09C7-09C8, U+09CB-09CE, U+09D7, U+09DC-09DD, U+09DF-09E3, U+09E6-09FE;
|
||||
unicode-range:
|
||||
U+0980-0983, U+0985-098C, U+098F-0990, U+0993-09A8, U+09AA-09B0, U+09B2, U+09B6-09B9, U+09BC-09C4, U+09C7-09C8, U+09CB-09CE, U+09D7, U+09DC-09DD, U+09DF-09E3, U+09E6-09FE;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -14,7 +15,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0B82-0B83, U+0B85-0B8A, U+0B8E-0B90, U+0B92-0B95, U+0B99-0B9A, U+0B9C, U+0B9E-0B9F, U+0BA3-0BA4, U+0BA8-0BAA, U+0BAE-0BB9, U+0BBE-0BC2, U+0BC6-0BC8, U+0BCA-0BCD, U+0BD0, U+0BD7, U+0BE6-0BFA;
|
||||
unicode-range:
|
||||
U+0B82-0B83, U+0B85-0B8A, U+0B8E-0B90, U+0B92-0B95, U+0B99-0B9A, U+0B9C, U+0B9E-0B9F, U+0BA3-0BA4, U+0BA8-0BAA, U+0BAE-0BB9, U+0BBE-0BC2, U+0BC6-0BC8, U+0BCA-0BCD, U+0BD0, U+0BD7, U+0BE6-0BFA;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -22,7 +24,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0C00-0C0C, U+0C0E-0C10, U+0C12-0C28, U+0C2A-0C39, U+0C3C-0C44, U+0C46-0C48, U+0C4A-0C4D, U+0C55-0C56, U+0C58-0C5A, U+0C5D, U+0C60-0C63, U+0C66-0C6F, U+0C77-0C7F;
|
||||
unicode-range:
|
||||
U+0C00-0C0C, U+0C0E-0C10, U+0C12-0C28, U+0C2A-0C39, U+0C3C-0C44, U+0C46-0C48, U+0C4A-0C4D, U+0C55-0C56, U+0C58-0C5A, U+0C5D, U+0C60-0C63, U+0C66-0C6F, U+0C77-0C7F;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -30,7 +33,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0C80-0C8C, U+0C8E-0C90, U+0C92-0CA8, U+0CAA-0CB3, U+0CB5-0CB9, U+0CBC-0CC4, U+0CC6-0CC8, U+0CCA-0CCD, U+0CD5-0CD6, U+0CDD-0CDE, U+0CE0-0CE3, U+0CE6-0CEF, U+0CF1-0CF3;
|
||||
unicode-range:
|
||||
U+0C80-0C8C, U+0C8E-0C90, U+0C92-0CA8, U+0CAA-0CB3, U+0CB5-0CB9, U+0CBC-0CC4, U+0CC6-0CC8, U+0CCA-0CCD, U+0CD5-0CD6, U+0CDD-0CDE, U+0CE0-0CE3, U+0CE6-0CEF, U+0CF1-0CF3;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -46,7 +50,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0A81-0A83, U+0A85-0A8D, U+0A8F-0A91, U+0A93-0AA8, U+0AAA-0AB0, U+0AB2-0AB3, U+0AB5-0AB9, U+0ABC-0AC5, U+0AC7-0AC9, U+0ACB-0ACD, U+0AD0, U+0AE0-0AE3, U+0AE6-0AF1, U+0AF9-0AFF;
|
||||
unicode-range:
|
||||
U+0A81-0A83, U+0A85-0A8D, U+0A8F-0A91, U+0A93-0AA8, U+0AAA-0AB0, U+0AB2-0AB3, U+0AB5-0AB9, U+0ABC-0AC5, U+0AC7-0AC9, U+0ACB-0ACD, U+0AD0, U+0AE0-0AE3, U+0AE6-0AF1, U+0AF9-0AFF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -54,7 +59,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0A01-0A03, U+0A05-0A0A, U+0A0F-0A10, U+0A13-0A28, U+0A2A-0A30, U+0A32-0A33, U+0A35-0A36, U+0A38-0A39, U+0A3C, U+0A3E-0A42, U+0A47-0A48, U+0A4B-0A4D, U+0A51, U+0A59-0A5C, U+0A5E, U+0A66-0A76;
|
||||
unicode-range:
|
||||
U+0A01-0A03, U+0A05-0A0A, U+0A0F-0A10, U+0A13-0A28, U+0A2A-0A30, U+0A32-0A33, U+0A35-0A36, U+0A38-0A39, U+0A3C, U+0A3E-0A42, U+0A47-0A48, U+0A4B-0A4D, U+0A51, U+0A59-0A5C, U+0A5E, U+0A66-0A76;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -62,7 +68,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0B01-0B03, U+0B05-0B0C, U+0B0F-0B10, U+0B13-0B28, U+0B2A-0B30, U+0B32-0B33, U+0B35-0B39, U+0B3C-0B44, U+0B47-0B48, U+0B4B-0B4D, U+0B55-0B57, U+0B5C-0B5D, U+0B5F-0B63, U+0B66-0B77;
|
||||
unicode-range:
|
||||
U+0B01-0B03, U+0B05-0B0C, U+0B0F-0B10, U+0B13-0B28, U+0B2A-0B30, U+0B32-0B33, U+0B35-0B39, U+0B3C-0B44, U+0B47-0B48, U+0B4B-0B4D, U+0B55-0B57, U+0B5C-0B5D, U+0B5F-0B63, U+0B66-0B77;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -70,7 +77,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0D81-0D83, U+0D85-0D96, U+0D9A-0DB1, U+0DB3-0DBB, U+0DBD, U+0DC0-0DC6, U+0DCA, U+0DCF-0DD4, U+0DD6, U+0DD8-0DDF, U+0DE6-0DEF, U+0DF2-0DF4, U+111E1-111F4;
|
||||
unicode-range:
|
||||
U+0D81-0D83, U+0D85-0D96, U+0D9A-0DB1, U+0DB3-0DBB, U+0DBD, U+0DC0-0DC6, U+0DCA, U+0DCF-0DD4, U+0DD6, U+0DD8-0DDF, U+0DE6-0DEF, U+0DF2-0DF4, U+111E1-111F4;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -86,7 +94,9 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0E81-0E82, U+0E84, U+0E86-0E8A, U+0E8C-0EA3, U+0EA5, U+0EA7-0EBD, U+0EC0-0EC4, U+0EC6, U+0EC8-0ECE, U+0ED0-0ED9, U+0EDC-0EDF;
|
||||
unicode-range:
|
||||
U+0E81-0E82, U+0E84, U+0E86-0E8A, U+0E8C-0EA3, U+0EA5, U+0EA7-0EBD, U+0EC0-0EC4, U+0EC6, U+0EC8-0ECE, U+0ED0-0ED9,
|
||||
U+0EDC-0EDF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -118,7 +128,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+1200-1248, U+124A-124D, U+1250-1256, U+1258, U+125A-125D, U+1260-1288, U+128A-128D, U+1290-12B0, U+12B2-12B5, U+12B8-12BE, U+12C0, U+12C2-12C5, U+12C8-12D6, U+12D8-1310, U+1312-1315, U+1318-135A, U+135D-137C, U+1380-1399, U+2D80-2D96, U+2DA0-2DA6, U+2DA8-2DAE, U+2DB0-2DB6, U+2DB8-2DBE, U+2DC0-2DC6, U+2DC8-2DCE, U+2DD0-2DD6, U+2DD8-2DDE, U+AB01-AB06, U+AB09-AB0E, U+AB11-AB16, U+AB20-AB26, U+AB28-AB2E, U+1E7E0-1E7E6, U+1E7E8-1E7EB, U+1E7ED-1E7EE, U+1E7F0-1E7FE;
|
||||
unicode-range:
|
||||
U+1200-1248, U+124A-124D, U+1250-1256, U+1258, U+125A-125D, U+1260-1288, U+128A-128D, U+1290-12B0, U+12B2-12B5, U+12B8-12BE, U+12C0, U+12C2-12C5, U+12C8-12D6, U+12D8-1310, U+1312-1315, U+1318-135A, U+135D-137C, U+1380-1399, U+2D80-2D96, U+2DA0-2DA6, U+2DA8-2DAE, U+2DB0-2DB6, U+2DB8-2DBE, U+2DC0-2DC6, U+2DC8-2DCE, U+2DD0-2DD6, U+2DD8-2DDE, U+AB01-AB06, U+AB09-AB0E, U+AB11-AB16, U+AB20-AB26, U+AB28-AB2E, U+1E7E0-1E7E6, U+1E7E8-1E7EB, U+1E7ED-1E7EE, U+1E7F0-1E7FE;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Fallback';
|
||||
@@ -198,7 +209,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+2190-2199, U+2300-230F, U+2311-2315, U+2317, U+231C-231F, U+2322-2323, U+2329-232A, U+232C-2335, U+237C, U+2380-2394, U+2396-239A, U+23AF, U+23BE-23CD, U+23D0-23DB, U+23E2-23E8, U+2460-24FF, U+25CC, U+260A-260D, U+2613, U+2624-262F, U+2638-263B, U+263D-2653, U+2669-267E, U+2690-269D, U+26A2-26A9, U+26AD-26BC, U+26CE, U+26E2-26FF, U+271D-2721, U+2776-2793, U+1F100-1F10C, U+1F110-1F16C, U+1F170-1F190, U+1F19B-1F1AC, U+1F546-1F549, U+1F54F, U+1F610, U+1F700-1F773;
|
||||
unicode-range:
|
||||
U+2190-2199, U+2300-230F, U+2311-2315, U+2317, U+231C-231F, U+2322-2323, U+2329-232A, U+232C-2335, U+237C, U+2380-2394, U+2396-239A, U+23AF, U+23BE-23CD, U+23D0-23DB, U+23E2-23E8, U+2460-24FF, U+25CC, U+260A-260D, U+2613, U+2624-262F, U+2638-263B, U+263D-2653, U+2669-267E, U+2690-269D, U+26A2-26A9, U+26AD-26BC, U+26CE, U+26E2-26FF, U+271D-2721, U+2776-2793, U+1F100-1F10C, U+1F110-1F16C, U+1F170-1F190, U+1F19B-1F1AC, U+1F546-1F549, U+1F54F, U+1F610, U+1F700-1F773;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Symbols';
|
||||
@@ -206,7 +218,8 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+21AF, U+21E6-21F0, U+21F3, U+2316, U+2318, U+231A-231B, U+2324-2328, U+232B, U+237B, U+237D-237F, U+23CE-23CF, U+23E9-23EA, U+23ED-23EF, U+23F1-2426, U+2440-244A, U+25A0-25CB, U+25CD-2609, U+260E-2612, U+2614-2623, U+2630-2637, U+263C, U+2654-2668, U+267F-268F, U+269E-26A1, U+26AA-26AC, U+26BD-26CD, U+26CF-26E1, U+2700-2704, U+2706-2709, U+270B-271C, U+2722-2727, U+2729-274B, U+274D, U+274F-2753, U+2756-2775, U+2794, U+2798-27AF, U+27B1-27BE, U+2800-28FF, U+2B00-2B0D, U+2B12-2B2F, U+2B4D-2B73, U+2B76-2B95, U+2B97-2BFD, U+2BFF, U+1F000-1F02B, U+1F030-1F093, U+1F0A0-1F0AE, U+1F0B1-1F0BF, U+1F0C1-1F0CF, U+1F0D1-1F0F5, U+1F10D-1F10F, U+1F16D-1F16F, U+1F1AD, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F321-1F32C, U+1F336, U+1F378, U+1F37D, U+1F393-1F39F, U+1F3A7, U+1F3AC-1F3AE, U+1F3C2, U+1F3C4, U+1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4DA, U+1F4DF, U+1F4E4-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50A, U+1F50D, U+1F512-1F513, U+1F53E-1F545, U+1F54A, U+1F550-1F579, U+1F57B-1F594, U+1F597-1F5A3, U+1F5A5-1F5FA, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CB, U+1F6CD-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F774-1F776, U+1F77B-1F7D9, U+1F7E0-1F7EB, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, U+1F900-1F90B, U+1F93B, U+1F946, U+1FA00-1FA53, U+1FA60-1FA6D, U+1FA70-1FA74, U+1FA78-1FA7A, U+1FA80-1FA86, U+1FA90-1FAA8, U+1FAB0-1FAB6, U+1FAC0-1FAC2, U+1FAD0-1FAD6, U+1FB00-1FB92, U+1FB94-1FBCA, U+1FBF0-1FBF9;
|
||||
unicode-range:
|
||||
U+21AF, U+21E6-21F0, U+21F3, U+2316, U+2318, U+231A-231B, U+2324-2328, U+232B, U+237B, U+237D-237F, U+23CE-23CF, U+23E9-23EA, U+23ED-23EF, U+23F1-2426, U+2440-244A, U+25A0-25CB, U+25CD-2609, U+260E-2612, U+2614-2623, U+2630-2637, U+263C, U+2654-2668, U+267F-268F, U+269E-26A1, U+26AA-26AC, U+26BD-26CD, U+26CF-26E1, U+2700-2704, U+2706-2709, U+270B-271C, U+2722-2727, U+2729-274B, U+274D, U+274F-2753, U+2756-2775, U+2794, U+2798-27AF, U+27B1-27BE, U+2800-28FF, U+2B00-2B0D, U+2B12-2B2F, U+2B4D-2B73, U+2B76-2B95, U+2B97-2BFD, U+2BFF, U+1F000-1F02B, U+1F030-1F093, U+1F0A0-1F0AE, U+1F0B1-1F0BF, U+1F0C1-1F0CF, U+1F0D1-1F0F5, U+1F10D-1F10F, U+1F16D-1F16F, U+1F1AD, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F321-1F32C, U+1F336, U+1F378, U+1F37D, U+1F393-1F39F, U+1F3A7, U+1F3AC-1F3AE, U+1F3C2, U+1F3C4, U+1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4DA, U+1F4DF, U+1F4E4-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50A, U+1F50D, U+1F512-1F513, U+1F53E-1F545, U+1F54A, U+1F550-1F579, U+1F57B-1F594, U+1F597-1F5A3, U+1F5A5-1F5FA, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CB, U+1F6CD-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F774-1F776, U+1F77B-1F7D9, U+1F7E0-1F7EB, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, U+1F900-1F90B, U+1F93B, U+1F946, U+1FA00-1FA53, U+1FA60-1FA6D, U+1FA70-1FA74, U+1FA78-1FA7A, U+1FA80-1FA86, U+1FA90-1FAA8, U+1FAB0-1FAB6, U+1FAC0-1FAC2, U+1FAD0-1FAD6, U+1FB00-1FB92, U+1FB94-1FBCA, U+1FBF0-1FBF9;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fluxer Math';
|
||||
@@ -214,5 +227,6 @@
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+2102, U+210A-2113, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2200-22FF, U+27C0-27FF, U+2900-2AFF, U+1D400-1D454, U+1D456-1D49C, U+1D49E-1D49F, U+1D4A2, U+1D4A5-1D4A6, U+1D4A9-1D4AC, U+1D4AE-1D4B9, U+1D4BB, U+1D4BD-1D4C3, U+1D4C5-1D505, U+1D507-1D50A, U+1D50D-1D514, U+1D516-1D51C, U+1D51E-1D539, U+1D53B-1D53E, U+1D540-1D544, U+1D546, U+1D54A-1D550, U+1D552-1D6A5, U+1D6A8-1D7CB, U+1D7CE-1D7FF;
|
||||
unicode-range:
|
||||
U+2102, U+210A-2113, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2200-22FF, U+27C0-27FF, U+2900-2AFF, U+1D400-1D454, U+1D456-1D49C, U+1D49E-1D49F, U+1D4A2, U+1D4A5-1D4A6, U+1D4A9-1D4AC, U+1D4AE-1D4B9, U+1D4BB, U+1D4BD-1D4C3, U+1D4C5-1D505, U+1D507-1D50A, U+1D50D-1D514, U+1D516-1D51C, U+1D51E-1D539, U+1D53B-1D53E, U+1D540-1D544, U+1D546, U+1D54A-1D550, U+1D552-1D6A5, U+1D6A8-1D7CB, U+1D7CE-1D7FF;
|
||||
}
|
||||
@@ -2,10 +2,44 @@
|
||||
|
||||
import {resolveRetryAfterMs} from '@app/features/messaging/utils/RetryAfterUtils';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import * as SlowmodeCommands from '@app/features/slowmode/commands/SlowmodeCommands';
|
||||
import Slowmode from '@app/features/slowmode/state/Slowmode';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import type {PendingMessageSend} from '@app/features/slowmode/commands/SlowmodeCommands';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
interface SlowmodeStore {
|
||||
clearChannel(channelId: string): void;
|
||||
getLastSendTimestamp(channelId: string): number | null;
|
||||
getSlowmodeRemaining(channelId: string, rateLimitPerUser: number): number;
|
||||
}
|
||||
|
||||
interface SlowmodeCommandModule {
|
||||
clampSlowmodeRetryAfterMs(retryAfterMs: number | null): number;
|
||||
confirmMessageSend(channelId: string, sentAt: string, pending?: PendingMessageSend): void;
|
||||
discardPendingMessageSend(channelId: string, pending: PendingMessageSend): void;
|
||||
recordPendingMessageSend(channelId: string): PendingMessageSend;
|
||||
updateSlowmodeRemaining(channelId: string, retryAfterMs: number): void;
|
||||
}
|
||||
|
||||
let Slowmode: SlowmodeStore;
|
||||
let SlowmodeCommands: SlowmodeCommandModule;
|
||||
|
||||
beforeAll(async () => {
|
||||
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
|
||||
try {
|
||||
const [slowmodeModule, commandModule, persistenceModule] = await Promise.all([
|
||||
import('@app/features/slowmode/state/Slowmode'),
|
||||
import('@app/features/slowmode/commands/SlowmodeCommands'),
|
||||
import('@app/features/platform/utils/MobXPersistence'),
|
||||
]);
|
||||
Slowmode = slowmodeModule.default;
|
||||
SlowmodeCommands = commandModule;
|
||||
await persistenceModule.awaitHydration('Slowmode');
|
||||
expect(debug).toHaveBeenCalledExactlyOnceWith('Store Slowmode hydrated from AppStorage and is now persisting.');
|
||||
} finally {
|
||||
debug.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
const CHANNEL_ID = '1234567890123456789';
|
||||
const RATE_LIMIT_PER_USER = 5;
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {installVoiceMenuTestBootstrap} from '@app/features/ui/action_menu/items/__fixtures__/VoiceMenuTestBootstrap';
|
||||
import type {VoiceParticipantMenuSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import {act, createElement} from 'react';
|
||||
import {createRoot, type Root} from 'react-dom/client';
|
||||
import {afterEach, expect, test, vi} from 'vitest';
|
||||
|
||||
vi.mock('@lingui/core/macro', () => {
|
||||
const descriptor = (value: unknown): unknown => (typeof value === 'string' ? {message: value} : value);
|
||||
return {msg: descriptor, t: descriptor, plural: () => '', select: () => '', selectOrdinal: () => ''};
|
||||
});
|
||||
vi.mock('@lingui/react/macro', () => ({
|
||||
Trans: () => null,
|
||||
useLingui: () => ({i18n: {_: (descriptor: {message?: string}) => descriptor.message ?? '', locale: 'en'}}),
|
||||
}));
|
||||
|
||||
installVoiceMenuTestBootstrap();
|
||||
|
||||
const {useVoiceParticipantMenuData} = await import('@app/features/ui/action_menu/items/VoiceParticipantMenuData');
|
||||
const {default: EntranceSoundListenerPrefs} = await import('@app/features/voice/state/EntranceSoundListenerPrefs');
|
||||
const {default: MediaEngine} = await import('@app/features/voice/engine/MediaEngineFacade');
|
||||
const {observer} = await import('mobx-react-lite');
|
||||
|
||||
(globalThis as {IS_REACT_ACT_ENVIRONMENT?: boolean}).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const TARGET_USER = {id: '111', username: 'target'} as unknown as User;
|
||||
const PARTICIPANT_SOURCE: VoiceParticipantMenuSource = {kind: 'participant'};
|
||||
const CAMERA_SOURCE: VoiceParticipantMenuSource = {kind: 'camera'};
|
||||
const noop = (): void => undefined;
|
||||
|
||||
interface MenuLeaf {
|
||||
label?: string;
|
||||
checked?: boolean;
|
||||
value?: number;
|
||||
items?: Array<MenuLeaf>;
|
||||
}
|
||||
|
||||
interface MenuGroup {
|
||||
items: Array<MenuLeaf>;
|
||||
}
|
||||
|
||||
const shadowedMediaEngineKeys: Array<string> = [];
|
||||
let root: Root | null = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
function shadowMediaEngine(key: string, value: unknown): void {
|
||||
Object.defineProperty(MediaEngine, key, {value, configurable: true, writable: true});
|
||||
shadowedMediaEngineKeys.push(key);
|
||||
}
|
||||
|
||||
function findLeaf(groups: Array<MenuGroup>, label: string): MenuLeaf | null {
|
||||
for (const group of groups) {
|
||||
for (const item of group.items) {
|
||||
if (item.label === label) return item;
|
||||
for (const child of item.items ?? []) {
|
||||
if (child.label === label) return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function renderMenu(
|
||||
options: Parameters<typeof useVoiceParticipantMenuData>[0],
|
||||
): Promise<{groups: () => Array<MenuGroup>}> {
|
||||
const latest: {groups: Array<MenuGroup>} = {groups: []};
|
||||
const Probe = observer(function Probe() {
|
||||
latest.groups = useVoiceParticipantMenuData(options).groups as unknown as Array<MenuGroup>;
|
||||
return null;
|
||||
});
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
await act(async () => {
|
||||
root?.render(createElement(Probe));
|
||||
});
|
||||
return {groups: () => latest.groups};
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
root?.unmount();
|
||||
});
|
||||
root = null;
|
||||
container?.remove();
|
||||
container = null;
|
||||
for (const key of shadowedMediaEngineKeys.splice(0)) {
|
||||
delete (MediaEngine as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
EntranceSoundListenerPrefs.reset(TARGET_USER.id);
|
||||
});
|
||||
|
||||
test('entrance-sound checkbox tracks the pref while the menu stays open', async () => {
|
||||
const menu = await renderMenu({
|
||||
user: TARGET_USER,
|
||||
surface: 'call-tile',
|
||||
source: PARTICIPANT_SOURCE,
|
||||
onClose: noop,
|
||||
});
|
||||
expect(findLeaf(menu.groups(), 'Mute entrance sound')?.checked).toBe(false);
|
||||
await act(async () => {
|
||||
EntranceSoundListenerPrefs.setMuted(TARGET_USER.id, true);
|
||||
});
|
||||
expect(findLeaf(menu.groups(), 'Mute entrance sound')?.checked).toBe(true);
|
||||
await act(async () => {
|
||||
EntranceSoundListenerPrefs.setVolume(TARGET_USER.id, 42);
|
||||
});
|
||||
expect(findLeaf(menu.groups(), 'Entrance sound volume')?.value).toBe(42);
|
||||
});
|
||||
|
||||
test('disable video locally is offered for another user camera tile in the local room', async () => {
|
||||
shadowMediaEngine('connected', true);
|
||||
shadowMediaEngine('connectionId', 'self-connection');
|
||||
shadowMediaEngine('channelId', 'channel-1');
|
||||
shadowMediaEngine('getVoiceStateByConnectionId', (connectionId: string) =>
|
||||
connectionId === 'target-connection' ? {channel_id: 'channel-1', user_id: TARGET_USER.id} : null,
|
||||
);
|
||||
const menu = await renderMenu({
|
||||
user: TARGET_USER,
|
||||
connectionId: 'target-connection',
|
||||
surface: 'call-tile',
|
||||
source: CAMERA_SOURCE,
|
||||
onClose: noop,
|
||||
});
|
||||
expect(findLeaf(menu.groups(), 'Disable video locally')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('offers a single inline user-volume slider and no nested secondary submenu wrapper', async () => {
|
||||
const REMOVED_SUBMENU_LABELS = [
|
||||
'Media controls',
|
||||
'Device controls',
|
||||
'Display options',
|
||||
'User actions',
|
||||
'Relationship',
|
||||
'Relationship actions',
|
||||
'Moderation',
|
||||
'Moderation actions',
|
||||
'Advanced',
|
||||
'Advanced actions',
|
||||
];
|
||||
shadowMediaEngine('connected', true);
|
||||
shadowMediaEngine('connectionId', 'self-connection');
|
||||
shadowMediaEngine('channelId', 'channel-1');
|
||||
shadowMediaEngine('getVoiceStateByConnectionId', (connectionId: string) =>
|
||||
connectionId === 'target-connection' ? {channel_id: 'channel-1', user_id: TARGET_USER.id} : null,
|
||||
);
|
||||
const menu = await renderMenu({
|
||||
user: TARGET_USER,
|
||||
connectionId: 'target-connection',
|
||||
surface: 'participant-list',
|
||||
source: PARTICIPANT_SOURCE,
|
||||
onClose: noop,
|
||||
});
|
||||
const groups = menu.groups();
|
||||
const topLevelItems = groups.flatMap((group) => group.items);
|
||||
const topLevelSliders = topLevelItems.filter((item) => typeof item.value === 'number');
|
||||
expect(topLevelSliders).toHaveLength(1);
|
||||
expect(topLevelSliders[0]?.label).toBe('User volume');
|
||||
const submenuLabels = topLevelItems.filter((item) => Array.isArray(item.items)).map((item) => item.label);
|
||||
expect(submenuLabels.every((label) => label === 'Entrance sound')).toBe(true);
|
||||
for (const label of REMOVED_SUBMENU_LABELS) {
|
||||
expect(findLeaf(groups, label)).toBeNull();
|
||||
}
|
||||
});
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {installVoiceMenuTestBootstrap} from '@app/features/ui/action_menu/items/__fixtures__/VoiceMenuTestBootstrap';
|
||||
import type {VoiceParticipantMenuScreenShareSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {expect, test, vi} from 'vitest';
|
||||
|
||||
vi.mock('@lingui/core/macro', () => {
|
||||
const descriptor = (value: unknown): unknown => (typeof value === 'string' ? {message: value} : value);
|
||||
return {msg: descriptor, t: descriptor, plural: () => '', select: () => '', selectOrdinal: () => ''};
|
||||
});
|
||||
vi.mock('@lingui/react/macro', () => ({
|
||||
Trans: () => null,
|
||||
useLingui: () => ({i18n: {_: (descriptor: {message?: string}) => descriptor.message ?? '', locale: 'en'}}),
|
||||
}));
|
||||
vi.mock('@app/features/voice/components/ActiveScreenShareMenu', () => ({
|
||||
changeActiveScreenShare: vi.fn(async () => undefined),
|
||||
stopActiveScreenShare: vi.fn(async () => undefined),
|
||||
}));
|
||||
vi.mock('@app/features/voice/components/modals/ScreenSharePickerModal', () => ({
|
||||
openScreenSharePreviewPrivacyModal: vi.fn(),
|
||||
}));
|
||||
vi.mock('@app/features/voice/engine/MediaEngineFacade', () => ({
|
||||
default: {
|
||||
applyLocalAudioPreferencesForUser: vi.fn(),
|
||||
getVoiceStateByConnectionId: () => null,
|
||||
connectionId: null,
|
||||
},
|
||||
}));
|
||||
vi.mock('@app/features/voice/state/PopoutWindowManager', () => ({
|
||||
default: {openTilePopout: vi.fn()},
|
||||
isVoicePopoutSupported: () => false,
|
||||
}));
|
||||
vi.mock('@app/features/voice/state/StreamAudioPrefs', () => ({
|
||||
default: {setMuted: vi.fn(), setVolume: vi.fn()},
|
||||
}));
|
||||
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
|
||||
default: {showMyOwnScreenShare: false, pauseOwnScreenSharePreviewOnUnfocus: false},
|
||||
}));
|
||||
vi.mock('@app/features/voice/commands/VoiceSettingsCommands', () => ({
|
||||
update: vi.fn(),
|
||||
}));
|
||||
|
||||
installVoiceMenuTestBootstrap();
|
||||
|
||||
const {buildVoiceParticipantStreamMenu} = await import(
|
||||
'@app/features/ui/action_menu/items/VoiceParticipantStreamMenuBuilder'
|
||||
);
|
||||
|
||||
const i18n = {
|
||||
locale: 'en',
|
||||
_: (descriptor: {message?: string}) => descriptor.message ?? '',
|
||||
} as unknown as I18n;
|
||||
|
||||
interface MenuLeaf {
|
||||
label?: string;
|
||||
items?: Array<MenuLeaf>;
|
||||
}
|
||||
|
||||
function streamMenu(source: VoiceParticipantMenuScreenShareSource): Array<{items: Array<MenuLeaf>}> {
|
||||
return buildVoiceParticipantStreamMenu({
|
||||
i18n,
|
||||
userId: '111',
|
||||
channelId: null,
|
||||
participantIdentity: 'user_111_conn',
|
||||
displayName: 'Alice',
|
||||
isCurrentUserConnectedToVoice: false,
|
||||
source,
|
||||
streamVolume: 100,
|
||||
isStreamMuted: false,
|
||||
showMyOwnScreenShare: false,
|
||||
pauseOwnScreenSharePreviewOnUnfocus: false,
|
||||
onClose: () => undefined,
|
||||
}) as unknown as Array<{items: Array<MenuLeaf>}>;
|
||||
}
|
||||
|
||||
function findLeaf(groups: Array<{items: Array<MenuLeaf>}>, label: string): MenuLeaf | null {
|
||||
for (const group of groups) {
|
||||
for (const item of group.items) {
|
||||
if (item.label === label) return item;
|
||||
for (const child of item.items ?? []) {
|
||||
if (child.label === label) return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const OWN_STREAM_SOURCE: VoiceParticipantMenuScreenShareSource = {
|
||||
kind: 'screen-share',
|
||||
streamKey: 'stream-key',
|
||||
state: {kind: 'own'},
|
||||
};
|
||||
|
||||
const WATCHED_REMOTE_STREAM_SOURCE: VoiceParticipantMenuScreenShareSource = {
|
||||
kind: 'screen-share',
|
||||
streamKey: 'stream-key',
|
||||
state: {kind: 'remote-watched', hasAudio: true, onStopWatching: () => undefined},
|
||||
};
|
||||
|
||||
test('own stream keeps a More options submenu with the screen-share preferences', () => {
|
||||
const groups = streamMenu(OWN_STREAM_SOURCE);
|
||||
const moreOptions = findLeaf(groups, 'More options');
|
||||
expect(moreOptions).not.toBeNull();
|
||||
expect(findLeaf(groups, 'Show My Screen Share')).not.toBeNull();
|
||||
expect(findLeaf(groups, 'Report Problem')).toBeNull();
|
||||
});
|
||||
|
||||
test('remote watched stream omits the now-empty More options submenu and keeps audio controls', () => {
|
||||
const groups = streamMenu(WATCHED_REMOTE_STREAM_SOURCE);
|
||||
expect(findLeaf(groups, 'More options')).toBeNull();
|
||||
expect(findLeaf(groups, 'Mute')).not.toBeNull();
|
||||
expect(findLeaf(groups, 'Stream volume')).not.toBeNull();
|
||||
expect(findLeaf(groups, 'Report Problem')).toBeNull();
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const HARNESS_ENDPOINT = 'https://primary.test/api';
|
||||
|
||||
export function installVoiceMenuTestBootstrap(): void {
|
||||
const host = globalThis as unknown as {window?: Record<string, unknown>};
|
||||
if (typeof host.window === 'undefined') {
|
||||
host.window = host as unknown as Record<string, unknown>;
|
||||
}
|
||||
host.window.__FLUXER_BOOTSTRAP__ = {
|
||||
config: {
|
||||
releaseChannel: 'stable',
|
||||
bootstrapApiEndpoint: HARNESS_ENDPOINT,
|
||||
bootstrapApiPublicEndpoint: HARNESS_ENDPOINT,
|
||||
},
|
||||
instance: {
|
||||
api_code_version: Number.MAX_SAFE_INTEGER,
|
||||
endpoints: {
|
||||
api: HARNESS_ENDPOINT,
|
||||
api_client: HARNESS_ENDPOINT,
|
||||
api_public: HARNESS_ENDPOINT,
|
||||
gateway: 'wss://gateway.primary.test',
|
||||
media: 'https://media.primary.test',
|
||||
static_cdn: 'https://cdn.primary.test',
|
||||
marketing: 'https://primary.test',
|
||||
admin: 'https://admin.primary.test',
|
||||
invite: 'https://primary.test/invite',
|
||||
gift: 'https://primary.test/gift',
|
||||
webapp: 'https://app.primary.test',
|
||||
upload_relay: 'https://upload.primary.test',
|
||||
},
|
||||
captcha: {provider: 'none', hcaptcha_site_key: null, turnstile_site_key: null},
|
||||
features: {
|
||||
voice_enabled: false,
|
||||
stripe_enabled: false,
|
||||
self_hosted: false,
|
||||
presigned_attachment_uploads: false,
|
||||
emails_enabled: false,
|
||||
},
|
||||
gif: {provider: 'klipy', display_name: 'Klipy', attribution_required: false},
|
||||
sso: {enabled: false, enforced: false, display_name: null, redirect_uri: ''},
|
||||
registration: {mode: 'open', admin_registration_urls_enabled: true},
|
||||
community: {single_community: false, single_community_guild_id: null, direct_messages_disabled: false},
|
||||
services: {gif_enabled: true, youtube_enabled: false, bluesky_enabled: false},
|
||||
limits: undefined,
|
||||
push: {public_vapid_key: null},
|
||||
app_public: {
|
||||
branding: {
|
||||
product_name: 'Fluxer',
|
||||
icon_url: null,
|
||||
symbol_url: null,
|
||||
logo_url: null,
|
||||
wordmark_url: null,
|
||||
favicon_url: null,
|
||||
theme_color: null,
|
||||
},
|
||||
setup: {configured: true, admin_url: null},
|
||||
legal: {terms_url: null, privacy_url: null},
|
||||
registration: {collect_date_of_birth: true},
|
||||
},
|
||||
},
|
||||
geoip: {
|
||||
countryCode: null,
|
||||
regionCode: null,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
ageRestrictedGeos: [],
|
||||
ageBlockedGeos: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,9 +6,11 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
async function loadTick() {
|
||||
vi.resetModules();
|
||||
const {Logger} = await import('@app/features/platform/utils/AppLogger');
|
||||
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
|
||||
const {default: Window} = await import('@app/features/window/state/Window');
|
||||
const {default: Tick} = await import('@app/features/ui/state/Tick');
|
||||
return {Tick, Window};
|
||||
return {debug, Tick, Window};
|
||||
}
|
||||
|
||||
describe('Tick', () => {
|
||||
@@ -17,14 +19,16 @@ describe('Tick', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps counting while the window is visible but not focused', async () => {
|
||||
const {Tick, Window} = await loadTick();
|
||||
const {debug, Tick, Window} = await loadTick();
|
||||
const dispose = autorun(() => Tick.nowSecond);
|
||||
const started = Tick.nowSecond;
|
||||
Window.setFocused(false);
|
||||
expect(debug).toHaveBeenCalledExactlyOnceWith('Window focus changed: false');
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(Window.visible).toBe(true);
|
||||
expect(Tick.nowSecond).toBe(started + 3);
|
||||
@@ -32,9 +36,10 @@ describe('Tick', () => {
|
||||
});
|
||||
|
||||
it('stops counting while the window is hidden', async () => {
|
||||
const {Tick, Window} = await loadTick();
|
||||
const {debug, Tick, Window} = await loadTick();
|
||||
const dispose = autorun(() => Tick.nowSecond);
|
||||
Window.setVisible(false);
|
||||
expect(debug).toHaveBeenCalledExactlyOnceWith('Window visibility changed: false');
|
||||
const stopped = Tick.nowSecond;
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(Tick.nowSecond).toBe(stopped);
|
||||
@@ -42,24 +47,28 @@ describe('Tick', () => {
|
||||
});
|
||||
|
||||
it('resynchronises to wall clock as soon as the window becomes visible again', async () => {
|
||||
const {Tick, Window} = await loadTick();
|
||||
const {debug, Tick, Window} = await loadTick();
|
||||
const dispose = autorun(() => Tick.nowSecond);
|
||||
Window.setVisible(false);
|
||||
const stopped = Tick.nowSecond;
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(Tick.nowSecond).toBe(stopped);
|
||||
Window.setVisible(true);
|
||||
expect(debug).toHaveBeenNthCalledWith(1, 'Window visibility changed: false');
|
||||
expect(debug).toHaveBeenNthCalledWith(2, 'Window visibility changed: true');
|
||||
expect(debug).toHaveBeenCalledTimes(2);
|
||||
expect(Tick.nowSecond).toBe(stopped + 60);
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('does not run an interval while nothing observes the clock', async () => {
|
||||
const {Tick} = await loadTick();
|
||||
const {debug, Tick} = await loadTick();
|
||||
const idle = Tick.nowSecond;
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(Tick.nowSecond).toBe(idle);
|
||||
const dispose = autorun(() => Tick.nowSecond);
|
||||
expect(Tick.nowSecond).toBe(idle + 5);
|
||||
expect(debug).not.toHaveBeenCalled();
|
||||
dispose();
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,7 @@ interface FakeGraphHarness {
|
||||
interface FakeWindowHarness {
|
||||
requestedOptions: Array<AudioContextOptions | undefined>;
|
||||
contexts: Array<FakeAudioContext>;
|
||||
constructionErrors: Array<DOMException>;
|
||||
bodyListeners: Array<string>;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ function installFakeWindow(
|
||||
honourSampleRate?: boolean;
|
||||
} = {},
|
||||
): FakeWindowHarness {
|
||||
const harness: FakeWindowHarness = {requestedOptions: [], contexts: [], bodyListeners: []};
|
||||
const harness: FakeWindowHarness = {requestedOptions: [], contexts: [], constructionErrors: [], bodyListeners: []};
|
||||
const throwOn = config.throwOn ?? ((): boolean => false);
|
||||
const honourSampleRate = config.honourSampleRate ?? true;
|
||||
|
||||
@@ -64,7 +65,9 @@ function installFakeWindow(
|
||||
constructor(options?: AudioContextOptions) {
|
||||
harness.requestedOptions.push(options);
|
||||
if (throwOn(options)) {
|
||||
throw new DOMException('sample rate not supported', 'NotSupportedError');
|
||||
const error = new DOMException('sample rate not supported', 'NotSupportedError');
|
||||
harness.constructionErrors.push(error);
|
||||
throw error;
|
||||
}
|
||||
this.sampleRate = (honourSampleRate ? options?.sampleRate : undefined) ?? HARDWARE_SAMPLE_RATE;
|
||||
harness.contexts.push(this as unknown as FakeAudioContext);
|
||||
@@ -219,15 +222,30 @@ describe('VoiceSharedAudioContext', () => {
|
||||
it('returns null without throwing when the construction fails', async () => {
|
||||
const harness = installFakeWindow({throwOn: () => true});
|
||||
const {getSharedVoiceAudioContext} = await loadModule();
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
try {
|
||||
let context: AudioContext | null | undefined;
|
||||
expect(() => {
|
||||
context = getSharedVoiceAudioContext();
|
||||
}).not.toThrow();
|
||||
|
||||
let context: AudioContext | null | undefined;
|
||||
expect(() => {
|
||||
context = getSharedVoiceAudioContext();
|
||||
}).not.toThrow();
|
||||
|
||||
expect(context).toBeNull();
|
||||
expect(harness.contexts).toHaveLength(0);
|
||||
expect(harness.requestedOptions).toHaveLength(1);
|
||||
expect(context).toBeNull();
|
||||
expect(harness.contexts).toHaveLength(0);
|
||||
expect(harness.requestedOptions).toEqual([{latencyHint: 'interactive'}]);
|
||||
expect(harness.constructionErrors).toHaveLength(1);
|
||||
expect(warning).toHaveBeenCalledTimes(1);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^%c\[\d{2}:\d{2}:\d{2}\] \[VoiceSharedAudioContext\] \[Warn\]$/),
|
||||
'color:#ffc107;font-weight:normal',
|
||||
'Failed to construct voice AudioContext',
|
||||
{
|
||||
options: {latencyHint: 'interactive'},
|
||||
error: harness.constructionErrors[0],
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
warning.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('caches the context across calls and rebuilds it once it has closed', async () => {
|
||||
@@ -267,10 +285,25 @@ describe('VoiceSharedAudioContext', () => {
|
||||
});
|
||||
|
||||
it('createVoiceAudioContext returns null instead of throwing when the rate is rejected', async () => {
|
||||
installFakeWindow({throwOn: (options) => options?.sampleRate !== undefined});
|
||||
const harness = installFakeWindow({throwOn: (options) => options?.sampleRate !== undefined});
|
||||
const {createVoiceAudioContext} = await loadModule();
|
||||
|
||||
expect(createVoiceAudioContext({latencyHint: 'interactive', sampleRate: 48000})).toBeNull();
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
try {
|
||||
expect(createVoiceAudioContext({latencyHint: 'interactive', sampleRate: 48000})).toBeNull();
|
||||
expect(harness.constructionErrors).toHaveLength(1);
|
||||
expect(warning).toHaveBeenCalledTimes(1);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^%c\[\d{2}:\d{2}:\d{2}\] \[VoiceSharedAudioContext\] \[Warn\]$/),
|
||||
'color:#ffc107;font-weight:normal',
|
||||
'Failed to construct voice AudioContext',
|
||||
{
|
||||
options: {latencyHint: 'interactive', sampleRate: 48000},
|
||||
error: harness.constructionErrors[0],
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
warning.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves the context untouched when the browser has no WaveShaper', async () => {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import type {GuildReadyData} from '@app/features/gateway/types/GatewayGuildTypes';
|
||||
import type {VoiceState} from '@app/features/gateway/types/GatewayVoiceTypes';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {autorun} from 'mobx';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
import {VoiceEngineV2AppVoiceStateAdapter} from './VoiceEngineV2AppVoiceStateAdapter';
|
||||
|
||||
function voiceState(overrides: Partial<VoiceState> = {}): VoiceState {
|
||||
@@ -59,16 +60,27 @@ describe('VoiceEngineV2AppVoiceStateAdapter', () => {
|
||||
});
|
||||
|
||||
it('does not reinsert locally removed stale connections', () => {
|
||||
const adapter = new VoiceEngineV2AppVoiceStateAdapter();
|
||||
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
|
||||
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeDefined();
|
||||
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
|
||||
try {
|
||||
const adapter = new VoiceEngineV2AppVoiceStateAdapter();
|
||||
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
|
||||
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeDefined();
|
||||
|
||||
adapter.removeVoiceStateConnection('stale-connection');
|
||||
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
|
||||
adapter.removeVoiceStateConnection('stale-connection');
|
||||
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
|
||||
|
||||
expect(adapter.isConnectionIgnored('stale-connection')).toBe(true);
|
||||
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeNull();
|
||||
expect(adapter.getAllVoiceStatesInChannel('guild-1', 'channel-1')['stale-connection']).toBeUndefined();
|
||||
expect(adapter.isConnectionIgnored('stale-connection')).toBe(true);
|
||||
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeNull();
|
||||
expect(adapter.getAllVoiceStatesInChannel('guild-1', 'channel-1')['stale-connection']).toBeUndefined();
|
||||
expect(debug).toHaveBeenCalledExactlyOnceWith('Ignored voice state update for locally removed connection', {
|
||||
guildId: 'guild-1',
|
||||
channelId: 'channel-1',
|
||||
userId: 'user-1',
|
||||
connectionId: 'stale-connection',
|
||||
});
|
||||
} finally {
|
||||
debug.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('publishes self-video true-to-false changes through MobX observation', () => {
|
||||
|
||||
@@ -28,7 +28,38 @@ AppStorage.subscribe(
|
||||
{key: 'VoiceSettings'},
|
||||
);
|
||||
|
||||
const {default: VoiceSettings} = await import('./VoiceSettings');
|
||||
async function loadVoiceSettings() {
|
||||
const [{Logger}, persistenceModule] = await Promise.all([
|
||||
import('@app/features/platform/utils/AppLogger'),
|
||||
import('@app/features/platform/utils/MobXPersistence'),
|
||||
]);
|
||||
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
|
||||
try {
|
||||
const [{default: VoiceSettings}, {default: MediaPermission}] = await Promise.all([
|
||||
import('./VoiceSettings'),
|
||||
import('@app/features/permissions/system/state/MediaPermission'),
|
||||
]);
|
||||
await Promise.all([
|
||||
persistenceModule.awaitHydration('MacPermissions'),
|
||||
persistenceModule.awaitHydration('VoiceSettings'),
|
||||
vi.waitFor(() => expect(MediaPermission.isInitialized()).toBe(true)),
|
||||
]);
|
||||
expect(debug).toHaveBeenCalledTimes(3);
|
||||
expect(debug).toHaveBeenCalledWith('Store MacPermissions hydrated from AppStorage and is now persisting.');
|
||||
expect(debug).toHaveBeenCalledWith('Initial permission state', {
|
||||
microphone: 'granted',
|
||||
camera: 'granted',
|
||||
micDenied: false,
|
||||
cameraDenied: false,
|
||||
});
|
||||
expect(debug).toHaveBeenCalledWith('Store VoiceSettings hydrated from AppStorage and is now persisting.');
|
||||
return VoiceSettings;
|
||||
} finally {
|
||||
debug.mockRestore();
|
||||
}
|
||||
}
|
||||
|
||||
const VoiceSettings = await loadVoiceSettings();
|
||||
|
||||
describe('AV1/HEVC screen-share opt-in', () => {
|
||||
it('rewrites a stored AV1 screen-share preference back to automatic on first launch', () => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import type {HardwareEncodeReport} from '@app/features/voice/utils/GpuEncoderCapabilities';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {afterEach, beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest';
|
||||
|
||||
let av1OptIn = false;
|
||||
let hevcOptIn = false;
|
||||
let desktop = true;
|
||||
let gpuReport: HardwareEncodeReport | null = null;
|
||||
let codecCapabilityInfo: MockInstance<(...args: Array<unknown>) => void>;
|
||||
|
||||
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
|
||||
default: {
|
||||
@@ -70,7 +72,22 @@ const ALL_SOFTWARE: HardwareEncodeReport = {
|
||||
};
|
||||
|
||||
describe('screen-share AV1/HEVC opt-in gate', () => {
|
||||
afterEach(() => {
|
||||
try {
|
||||
expect(codecCapabilityInfo.mock.calls.length).toBeGreaterThan(0);
|
||||
for (const call of codecCapabilityInfo.mock.calls) {
|
||||
expect(call).toEqual([
|
||||
'Codec capabilities probed',
|
||||
{capabilities: {vp8: true, vp9: true, h264: true, h265: true, av1: true}},
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
codecCapabilityInfo.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
codecCapabilityInfo = vi.spyOn(Logger.prototype, 'info').mockImplementation(() => undefined);
|
||||
av1OptIn = false;
|
||||
hevcOptIn = false;
|
||||
desktop = true;
|
||||
|
||||
Reference in new issue
Block a user