feat(mobile): advance Paca.Mobile to production excellence with rich design assets and core systems
- Generate high-resolution glassmorphism app icon and splash screen assets - Implement MobileIconSystem with semantic SF Symbols/Material glyph mappings - Add OLEDBlack theme and contrast tokens to MobileThemeSystem - Implement VideoSnifferParser.NegotiateBestQuality for multi-stream quality resolution - Enhance DiscoverPage, FavoritesPage, and SyncSettingsPage with production controls - Add Gate 103-114 test suites with 100% test pass rate
This commit is contained in:
parent
dbc50abb71
commit
f1949024fc
81 changed files with 8611 additions and 0 deletions
152
tests/Paca.Tests/Mobile/MobileCoreTests.cs
Normal file
152
tests/Paca.Tests/Mobile/MobileCoreTests.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using Paca.Mobile;
|
||||
|
||||
namespace Paca.Tests.Mobile;
|
||||
|
||||
/// <summary>Mobile.Core 순수 로직 — U5 완료 감지, 외부망 안내, U4 저장소(파일 시스템).</summary>
|
||||
public class MobileCoreTests
|
||||
{
|
||||
// ── U5 완료 알림 감지 ──
|
||||
|
||||
[Fact]
|
||||
public void 완료_전환_항목_ID_반환()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "다운로드 중"), ("b", "대기") };
|
||||
var next = new[] { ("a", "완료"), ("b", "다운로드 중") };
|
||||
Assert.Equal(new[] { "a" }, d.Detect(prev, next));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 이미_완료였던_항목은_재알림_없음()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "완료") };
|
||||
var next = new[] { ("a", "완료") };
|
||||
Assert.Empty(d.Detect(prev, next));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 첫_폴링에서_이미_완료된_항목은_무시() // 앱 시작 시 과거 완료 스팸 방지
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
Assert.Empty(d.Detect(Array.Empty<(string, string)>(), new[] { ("a", "완료") }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 목록에서_사라진_항목은_알림_없음()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "다운로드 중") };
|
||||
Assert.Empty(d.Detect(prev, Array.Empty<(string, string)>()));
|
||||
}
|
||||
|
||||
// ── 외부망 안내(MOBILE_PLAN §7) ──
|
||||
|
||||
[Fact]
|
||||
public void 미연결_안내()
|
||||
{
|
||||
var m = ConnectionTroubleshoot.Message(isConfigured: false, lastError: null);
|
||||
Assert.Contains("연결", m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 연결됐지만_실패_시_같은네트워크_VPN_안내()
|
||||
{
|
||||
var m = ConnectionTroubleshoot.Message(isConfigured: true, lastError: "No such host is known.");
|
||||
Assert.Contains("같은 네트워크", m);
|
||||
Assert.Contains("Tailscale", m); // 계획 §3: 자체 릴레이 대신 Tailscale 문서화
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 정상_시_빈_문자열()
|
||||
=> Assert.Equal("", ConnectionTroubleshoot.Message(isConfigured: true, lastError: null));
|
||||
|
||||
|
||||
// ── M1 공유 타겟(U1): 공유 텍스트 → URL 추출 ──
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://youtu.be/abc123", "https://youtu.be/abc123")] // URL 만
|
||||
[InlineData("재밌는 영상\nhttps://youtu.be/abc123", "https://youtu.be/abc123")] // 제목+URL (유튜브 공유 형식)
|
||||
[InlineData("http://x.com/a?b=1 입니다", "http://x.com/a?b=1")] // http + 쿼리
|
||||
[InlineData("a https://1.com b https://2.com", "https://1.com")] // 첫 번째 우선
|
||||
public void 공유_텍스트에서_URL_추출(string text, string expected)
|
||||
=> Assert.Equal(expected, SharePayload.ExtractUrl(text));
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("URL 없는 그냥 텍스트")]
|
||||
public void URL_없으면_null(string? text)
|
||||
=> Assert.Null(SharePayload.ExtractUrl(text));
|
||||
|
||||
// ── U4 저장소(임시 디렉터리로 파일 시스템 검증) ──
|
||||
|
||||
[Fact]
|
||||
public void OfflineStore_기록_목록_삭제_라운드트립()
|
||||
{
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), $"vd_core_{Guid.NewGuid():N}");
|
||||
var store = new OfflineStore(baseDir);
|
||||
Assert.Empty(store.List());
|
||||
|
||||
var p = store.PathFor("id1");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(p)!);
|
||||
File.WriteAllBytes(p, new byte[123]);
|
||||
store.Record("id1", "video.mp4");
|
||||
|
||||
Assert.True(store.Exists("id1"));
|
||||
var list = store.List();
|
||||
Assert.Single(list);
|
||||
Assert.Equal(("id1", "video.mp4", 123L), (list[0].Id, list[0].FileName, list[0].SizeBytes));
|
||||
|
||||
store.Delete("id1");
|
||||
Assert.False(store.Exists("id1"));
|
||||
Assert.Empty(store.List());
|
||||
|
||||
Directory.Delete(baseDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OfflineStore_재시작_시_매니페스트_유지()
|
||||
{
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), $"vd_core_{Guid.NewGuid():N}");
|
||||
var store = new OfflineStore(baseDir);
|
||||
var p = store.PathFor("id2");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(p)!);
|
||||
File.WriteAllBytes(p, new byte[10]);
|
||||
store.Record("id2", "keep.mp4");
|
||||
|
||||
var reopened = new OfflineStore(baseDir); // 새 인스턴스 = 앱 재시작
|
||||
Assert.True(reopened.Exists("id2"));
|
||||
Assert.Equal("keep.mp4", reopened.List().Single().FileName);
|
||||
|
||||
Directory.Delete(baseDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MobileSyncManager_네트워크_SSID_및_WiFi_필터_검증()
|
||||
{
|
||||
var config = new Paca.Mobile.Core.Sync.MobileSyncConfig
|
||||
{
|
||||
AutoSyncOnWifi = true,
|
||||
TargetSsid = "Home_5G"
|
||||
};
|
||||
var mgr = new Paca.Mobile.Core.Sync.MobileSyncManager(config);
|
||||
|
||||
Assert.True(mgr.CanSyncOnNetwork("Home_5G", isWifi: true));
|
||||
Assert.False(mgr.CanSyncOnNetwork("Public_Cafe", isWifi: true));
|
||||
Assert.False(mgr.CanSyncOnNetwork("Home_5G", isWifi: false)); // LTE/5G
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MobileSyncManager_동기화_트리거_및_상태_갱신_검증()
|
||||
{
|
||||
var mgr = new Paca.Mobile.Core.Sync.MobileSyncManager();
|
||||
Assert.Null(mgr.Status.LastSyncUtc);
|
||||
|
||||
bool ok = await mgr.TriggerSyncAsync(() => Task.FromResult(5));
|
||||
Assert.True(ok);
|
||||
Assert.NotNull(mgr.Status.LastSyncUtc);
|
||||
Assert.Equal(5, mgr.Status.CompletedDownloadsCount);
|
||||
Assert.Empty(mgr.Status.LastError);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue