Phase A: tab architecture integrated into MainWindow - tab strip UI, per-tab WebView2 lifecycle, new-window->tab w/ deferral, audio indicator/mute, session save/restore, shortcuts; E2E on real window (56 tests GREEN)
This commit is contained in:
parent
be01a116ca
commit
b6b7fc341b
7 changed files with 389 additions and 106 deletions
|
|
@ -8,7 +8,7 @@
|
|||
> - 1차(2026-08-13): Chrome/Firefox/Zen 3개 브라우저 단일 조사 → v1 초안
|
||||
> - **2차(2026-08-15): 5개 전문 조사팀 병렬 심층 조사** — ① Chrome+Edge ② Firefox+Vivaldi ③ Zen+Arc ④ Brave+Opera+Safari+기타(Floorp/Orion/Sidekick/Comet) ⑤ **WebView2 API 검증팀(Microsoft Learn 팩트체크)**. 총 80여 회 웹 검색 + 공식 문서 직접 열람.
|
||||
>
|
||||
> **확장 계획**: 모바일 앱 + PC↔모바일 연동은 **`MOBILE_PLAN.md`** 로 분리 (2026-08-15, 4개 조사팀 심층 조사 기반 — MAUI 10 컴패니언 앱, LAN QR 페어링, Core 엔진 재사용 검증 포함).
|
||||
> **확장 계획**: 모바일 앱 + PC↔모바일 연동은 **`MOBILE_PLAN.md`** 로 분리 (2026-08-15, 4개 조사팀 심층 조사 기반 — MAUI 10 컴패니언 앱, LAN QR 페어링, Core 엔진 재사용 검증 포함). 멀티플랫폼 배포(Win/macOS/Linux/모바일)는 **`DEPLOYMENT_PLAN.md`** 로 분리.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> **목표**: Windows 비디오 다운로더(내장 브라우저)에 **모바일 앱을 만들고 PC↔모바일을 연동**한다. 핵심 질문 — "모바일에서 이 앱과 무엇을 하게 할 것인가?"에 대한 조사 기반 답변.
|
||||
>
|
||||
> **작성일**: 2026-08-15 · **관련 문서**: `BROWSER_FEATURES_PLAN.md` (v2, 데스크톱 브라우저 기능)
|
||||
> **작성일**: 2026-08-15 · **관련 문서**: `BROWSER_FEATURES_PLAN.md` (v2, 데스크톱 브라우저 기능) · `DEPLOYMENT_PLAN.md` (Win/macOS/Linux/모바일 배포 체계)
|
||||
>
|
||||
> **조사 방법론**: 4개 전문 조사팀 병렬 심층 조사 — ① 모바일 브라우저 UX(Chrome/Safari/Firefox/Samsung/Brave 모바일) ② PC↔모바일 연동 패턴(20여 개 제품: Chrome Send Tab, Handoff, Phone Link, LocalSend, KDE Connect, Jellyfin 등) ③ 모바일 영상 생태계(iOS/Android 제약, 다운로더 앱 생존 모델, 미디어 서버) ④ 기술 스택 검증(**실제 코드베이스 grep 포함**). 총 73회 웹 검색 + 공식 문서 열람.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ public sealed class TabItem : INotifyPropertyChanged
|
|||
|
||||
/// <summary>CoreWebView2 (초기화 전 null)</summary>
|
||||
public Microsoft.Web.WebView2.Core.CoreWebView2? Core => View?.CoreWebView2;
|
||||
|
||||
private string _title = "새 탭";
|
||||
public string Title { get => _title; set => Set(ref _title, value); }
|
||||
|
||||
|
|
@ -45,6 +44,11 @@ public sealed class TabItem : INotifyPropertyChanged
|
|||
/// <summary>닫힌 시점의 인덱스(닫힌 탭 복원용). 열려 있는 동안은 무의미.</summary>
|
||||
internal int ClosedIndex { get; set; } = -1;
|
||||
|
||||
// ---- 탭별 일시 상태(앱 레이어 전용) ----
|
||||
internal string? PendingHistoryUrl; // SourceChanged → 타이틀 확정 사이의 기록 대기 URL
|
||||
internal string? PendingSocialUrl; // 소셜 카드 제목 갱신 대기 URL
|
||||
internal FindSession? FindSession; // 탭별 페이지 내 검색 세션 (cv 바인딩)
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@
|
|||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
|
|
@ -209,8 +210,68 @@
|
|||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ===== 탭 스트립 (Phase A) ===== -->
|
||||
<Border x:Name="TabStripRow" Grid.Row="1" Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1" Padding="8,4,8,0">
|
||||
<DockPanel>
|
||||
<Button x:Name="NewTabBtn" DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
Content="" FontSize="13" ToolTip="새 탭 (Ctrl+T)" Click="NewTab_Click"
|
||||
Margin="4,0,2,3" VerticalAlignment="Center"/>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled"
|
||||
VerticalAlignment="Bottom">
|
||||
<ItemsControl x:Name="TabList">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,4,0" MinWidth="130" MaxWidth="220">
|
||||
<Border x:Name="TabBg" CornerRadius="8,8,0,0"
|
||||
Background="{StaticResource GlassBg}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="1,1,1,0"
|
||||
Padding="10,7,4,7" MouseLeftButtonUp="Tab_Click"
|
||||
PreviewMouseDown="Tab_PreviewMouseDown"
|
||||
ToolTip="{Binding Url}">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource CardActionButton}"
|
||||
Content="" FontSize="9" Padding="5,3" Margin="4,0,0,0"
|
||||
Click="TabClose_Click" ToolTip="탭 닫기 (Ctrl+W)"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="AudioIcon" Text="" Style="{StaticResource FluentIcon}"
|
||||
FontSize="11" Margin="0,0,6,0" VerticalAlignment="Center"
|
||||
Visibility="Collapsed" Cursor="Hand"
|
||||
MouseLeftButtonUp="TabAudio_Click"/>
|
||||
<TextBlock Text="{Binding Title}" TextTrimming="CharacterEllipsis"
|
||||
FontSize="12" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive}" Value="True">
|
||||
<Setter TargetName="TabBg" Property="Background" Value="{StaticResource CardBg}"/>
|
||||
<Setter TargetName="TabBg" Property="BorderBrush" Value="{StaticResource AccentFadeBrush}"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsAudioPlaying}" Value="True">
|
||||
<Setter TargetName="AudioIcon" Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsMuted}" Value="True">
|
||||
<Setter TargetName="AudioIcon" Property="Text" Value=""/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsPinned}" Value="True">
|
||||
<Setter TargetName="TabBg" Property="Background" Value="{StaticResource AccentFadeBrush}"/>
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ===== 본문 ===== -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
|
@ -224,7 +285,7 @@
|
|||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" ClipToBounds="True" Background="Black">
|
||||
<wv2:WebView2 Name="Browser"/>
|
||||
<Grid x:Name="TabHost" Background="Black"/>
|
||||
</Border>
|
||||
|
||||
<!-- 페이지 내 검색 바 (T1-3): WebView2(airspace) 아래에 도킹 -->
|
||||
|
|
@ -320,7 +381,7 @@
|
|||
</Grid>
|
||||
|
||||
<!-- ===== 상태 표시줄 ===== -->
|
||||
<Border x:Name="StatusBar" Grid.Row="2" Background="{StaticResource PanelBg}"
|
||||
<Border x:Name="StatusBar" Grid.Row="3" Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0" Padding="16,5">
|
||||
<TextBlock Name="StatusText" Foreground="{StaticResource TextMuted}" FontSize="11"
|
||||
TextTrimming="CharacterEllipsis" Text="준비"/>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ using System.Windows.Interop;
|
|||
using System.Windows.Media;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using VideoDownloader.App.Browser;
|
||||
using VideoDownloader.App.Browser.Tabs;
|
||||
using VideoDownloader.App.Core;
|
||||
using VideoDownloader.App.Detection;
|
||||
using VideoDownloader.App.ViewModels;
|
||||
using VideoDownloader.Core.Config;
|
||||
using Wv2 = Microsoft.Web.WebView2.Wpf;
|
||||
using TabItem = VideoDownloader.App.Browser.Tabs.TabItem;
|
||||
|
||||
namespace VideoDownloader.App;
|
||||
|
||||
|
|
@ -23,18 +26,22 @@ public partial class MainWindow : Window
|
|||
private AppConfig _config;
|
||||
private int _totalRequests;
|
||||
private string _appLogPath = "";
|
||||
private string _currentTitle = "";
|
||||
|
||||
// ---- 탭 아키텍처 (Phase A) ----
|
||||
private CoreWebView2Environment? _env;
|
||||
private TabManager _tabs = null!;
|
||||
private readonly Dictionary<TabItem, Task> _tabInit = new();
|
||||
internal TabManager Tabs => _tabs;
|
||||
internal Wv2.WebView2? ActiveView => _tabs?.ActiveTab?.View;
|
||||
private CoreWebView2? Cv => _tabs?.ActiveTab?.Core;
|
||||
|
||||
private readonly ObservableCollection<Bookmark> _bookmarks = new();
|
||||
private readonly ObservableCollection<HistoryEntry> _history = new();
|
||||
private string? _pendingHistoryUrl;
|
||||
private string? _pendingSocialUrl; // 타이틀 확정 시 소셜 카드 제목 갱신용
|
||||
private bool _suggesting; // 주소창 인라인 자동완성 도중 (TextChanged 재귀 방지)
|
||||
|
||||
// T1-4 검색엔진 / T1-7 사이트별 줌 / T1-3 페이지 내 검색 / T1-11 전체화면
|
||||
private SearchEngineResolver _resolver = null!;
|
||||
private SiteZoomStore _zoomStore = null!;
|
||||
private FindSession? _findSession;
|
||||
private bool _fullscreen;
|
||||
private WindowState _stateBeforeFullscreen = WindowState.Normal;
|
||||
|
||||
|
|
@ -51,6 +58,8 @@ public partial class MainWindow : Window
|
|||
private const double MinExpandedWidth = 280;
|
||||
private const double MaxExpandedWidth = 900;
|
||||
|
||||
private static string SessionPath => Path.Combine(AppDataDir, "session.json");
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
@ -142,44 +151,144 @@ public partial class MainWindow : Window
|
|||
|
||||
internal static string ProfileFolder => Path.Combine(AppDataDir, "webview");
|
||||
|
||||
// ==================== 탭 생명주기 (Phase A) ====================
|
||||
|
||||
private async Task InitializeBrowserAsync()
|
||||
{
|
||||
CoreWebView2Environment? env = null;
|
||||
try
|
||||
{
|
||||
// 명시적 영속 프로필: 기본(exe 디렉터리) 폴더는 빌드/재배포 시 날아가 로그인이 풀림 → %AppData% 로 고정.
|
||||
Directory.CreateDirectory(ProfileFolder);
|
||||
env = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: ProfileFolder);
|
||||
var options = env.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
await Browser.EnsureCoreWebView2Async(env, options);
|
||||
_env = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: ProfileFolder);
|
||||
}
|
||||
catch (Exception ex) { Log($"WebView2 초기화 실패: {ex.Message}"); return; }
|
||||
|
||||
var cv = Browser.CoreWebView2;
|
||||
_tabs = new TabManager(CreateTabItem, AttachTabView);
|
||||
_tabs.ActiveChanged += OnActiveTabChanged;
|
||||
_tabs.TabRemoved += OnTabRemoved;
|
||||
TabList.ItemsSource = _tabs.Tabs;
|
||||
UpdatePrivateButton();
|
||||
|
||||
// T1-10 세션 복원: 저장된 탭이 있으면 복원, 아니면 홈 탭
|
||||
var snap = _config.RestoreSession ? TabSessionStore.Load(SessionPath) : null;
|
||||
if (snap != null && snap.Urls.Count > 0)
|
||||
_tabs.RestoreSession(snap);
|
||||
else
|
||||
_tabs.NewTab(_config.HomeUrl);
|
||||
UpdateNavState();
|
||||
}
|
||||
|
||||
private TabItem CreateTabItem() => new();
|
||||
|
||||
/// <summary>탭에 WebView2 뷰를 붙이고 비동기 초기화를 시작한다. (UI 스레드)</summary>
|
||||
private void AttachTabView(TabItem tab)
|
||||
{
|
||||
var view = new Wv2.WebView2 { DefaultBackgroundColor = System.Drawing.Color.Black };
|
||||
tab.View = view;
|
||||
TabHost.Children.Add(view);
|
||||
view.Visibility = tab.IsActive ? Visibility.Visible : Visibility.Collapsed;
|
||||
_tabInit[tab] = InitTabCoreAsync(tab);
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 + 탭별 이벤트 배선 + 초기 URL 탐색.</summary>
|
||||
private async Task InitTabCoreAsync(TabItem tab)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = _env!.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
await tab.View!.EnsureCoreWebView2Async(_env, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"탭 초기화 실패: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var cv = tab.Core!;
|
||||
cv.Settings.AreDevToolsEnabled = true;
|
||||
|
||||
// T1-3 네이티브 Find API 세션 (자체 검색 바)
|
||||
_findSession = new FindSession(new WebView2FindApi(cv, env!));
|
||||
_findSession.StateChanged += () => Dispatcher.BeginInvoke(new Action(() =>
|
||||
// T1-3 탭별 검색 세션 (네이티브 Find API)
|
||||
tab.FindSession = new FindSession(new WebView2FindApi(cv, _env!));
|
||||
tab.FindSession.StateChanged += () => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (_findSession != null) FindStatus.Text = _findSession.StatusText;
|
||||
if (tab.IsActive && tab.FindSession != null) FindStatus.Text = tab.FindSession.StatusText;
|
||||
}));
|
||||
|
||||
// T1-7 사이트별 줌: 네비 완료 시 저장된 배율 적용 (WebView2 는 사이트별 영속 미보장)
|
||||
// 탭별 네비게이션 상태
|
||||
cv.NavigationStarting += (_, _) => Dispatcher.BeginInvoke(new Action(() => tab.IsLoading = true));
|
||||
cv.NavigationCompleted += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { Browser.ZoomFactor = _zoomStore.Get(cv.Source); } catch { }
|
||||
tab.IsLoading = false;
|
||||
try { if (tab.View != null) tab.View.ZoomFactor = _zoomStore.Get(cv.Source); } catch { } // T1-7
|
||||
}));
|
||||
cv.HistoryChanged += (_, _) => UpdateNavState();
|
||||
|
||||
// 소셜/주소창/기록: 탭별 상태로 관리
|
||||
cv.SourceChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var url = cv.Source ?? "";
|
||||
tab.Url = url;
|
||||
if (tab.PendingHistoryUrl != null) RecordHistory(tab.PendingHistoryUrl, tab.PendingHistoryUrl);
|
||||
if (tab.IsActive && !AddressBox.IsKeyboardFocused) AddressBox.Text = url;
|
||||
tab.PendingHistoryUrl = url.StartsWith("about:", StringComparison.OrdinalIgnoreCase) ? null : url;
|
||||
DetectSocial(tab, url);
|
||||
}));
|
||||
cv.DocumentTitleChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var title = cv.DocumentTitle ?? "";
|
||||
tab.Title = string.IsNullOrWhiteSpace(title) ? (tab.Url.StartsWith("http") ? tab.Url : "새 탭") : title;
|
||||
if (tab.PendingHistoryUrl != null)
|
||||
{
|
||||
RecordHistory(string.IsNullOrWhiteSpace(title) ? tab.PendingHistoryUrl : title, tab.PendingHistoryUrl);
|
||||
tab.PendingHistoryUrl = null;
|
||||
}
|
||||
// 소셜 카드 제목을 페이지 타이틀로 갱신(출력 파일명 포함, 시작 전일 때)
|
||||
if (tab.PendingSocialUrl != null && !string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
_manager.RenameSocial(new Uri(tab.PendingSocialUrl), title);
|
||||
tab.PendingSocialUrl = null;
|
||||
}
|
||||
}));
|
||||
|
||||
cv.WebResourceResponseReceived += OnResponseReceived;
|
||||
// #4 팝업/새창을 현재 WebView2 로 강제 라우팅
|
||||
cv.NewWindowRequested += (_, ne) =>
|
||||
// T1-8 오디오 표시/뮤트
|
||||
try
|
||||
{
|
||||
try { if (ne.Uri != null) { ne.Handled = true; cv.Navigate(ne.Uri); } }
|
||||
catch { }
|
||||
cv.IsDocumentPlayingAudioChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { tab.IsAudioPlaying = cv.IsDocumentPlayingAudio; } catch { }
|
||||
}));
|
||||
cv.IsMutedChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { tab.IsMuted = cv.IsMuted; } catch { }
|
||||
}));
|
||||
}
|
||||
catch { } // 구현체/타이밍 이슈 대비
|
||||
|
||||
// 미디어 감지(탭 무관 전역)
|
||||
cv.WebResourceResponseReceived += OnResponseReceived;
|
||||
|
||||
// Phase A: 팝업/새창 → 새 탭 (deferral 필수 — 탭 생성이 비동기)
|
||||
cv.NewWindowRequested += async (_, ne) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var deferral = ne.GetDeferral();
|
||||
try
|
||||
{
|
||||
var newTab = _tabs.NewTab(activate: false); // URL 없이 생성 → 팝업 내용이 새 창으로 흘러들어옴
|
||||
await WaitTabReady(newTab);
|
||||
ne.NewWindow = newTab.Core;
|
||||
ne.Handled = true;
|
||||
}
|
||||
finally { deferral.Complete(); }
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { ne.Handled = true; if (ne.Uri != null) cv.Navigate(ne.Uri); } catch { }
|
||||
}
|
||||
};
|
||||
|
||||
// #13 페이지 내 다운로드(a[download])를 저장 폴더로 라우팅
|
||||
cv.DownloadStarting += (_, de) =>
|
||||
{
|
||||
|
|
@ -194,46 +303,90 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch { }
|
||||
};
|
||||
cv.HistoryChanged += (_, _) => UpdateNavState();
|
||||
cv.SourceChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (_pendingHistoryUrl != null) RecordHistory(_pendingHistoryUrl, _pendingHistoryUrl);
|
||||
var url = cv.Source ?? "";
|
||||
AddressBox.Text = url;
|
||||
_pendingHistoryUrl = url.StartsWith("about:", StringComparison.OrdinalIgnoreCase) ? null : url;
|
||||
DetectSocial(url);
|
||||
}));
|
||||
cv.DocumentTitleChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_currentTitle = cv.DocumentTitle ?? "";
|
||||
if (_pendingHistoryUrl != null)
|
||||
{
|
||||
RecordHistory(string.IsNullOrWhiteSpace(_currentTitle) ? _pendingHistoryUrl : _currentTitle, _pendingHistoryUrl);
|
||||
_pendingHistoryUrl = null;
|
||||
}
|
||||
// 소셜 카드 제목을 페이지 타이틀로 갱신(출력 파일명 포함, 시작 전일 때)
|
||||
if (_pendingSocialUrl != null && !string.IsNullOrWhiteSpace(_currentTitle))
|
||||
{
|
||||
_manager.RenameSocial(new Uri(_pendingSocialUrl), _currentTitle);
|
||||
_pendingSocialUrl = null;
|
||||
}
|
||||
}));
|
||||
|
||||
NavHome_Click(this, null!);
|
||||
// 초기 URL (세션 복원/빈 탭 제외)
|
||||
if (!string.IsNullOrEmpty(tab.Url) && (cv.Source == null || cv.Source.Length == 0))
|
||||
{
|
||||
try { cv.Navigate(tab.Url); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 완료 대기 (뉴윈도우 deferral 용).</summary>
|
||||
private async Task WaitTabReady(TabItem tab)
|
||||
{
|
||||
if (tab.Core != null) return;
|
||||
if (_tabInit.TryGetValue(tab, out var t)) { try { await t; } catch { } return; }
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (tab.Core == null && sw.ElapsedMilliseconds < 15000)
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
private void OnActiveTabChanged(TabItem tab)
|
||||
{
|
||||
foreach (var t in _tabs.Tabs)
|
||||
if (t.View != null) t.View.Visibility = ReferenceEquals(t, tab) ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (!AddressBox.IsKeyboardFocused) AddressBox.Text = tab.Url;
|
||||
UpdateNavState();
|
||||
}
|
||||
|
||||
private void OnTabRemoved(TabItem tab)
|
||||
{
|
||||
if (tab.View != null)
|
||||
{
|
||||
TabHost.Children.Remove(tab.View);
|
||||
tab.View = null; // 재오픈 시 새 뷰로 재생성(ReopenClosedTab → AttachTabView)
|
||||
}
|
||||
_tabInit.Remove(tab);
|
||||
}
|
||||
|
||||
private void UpdateNavState()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
if (cv == null) return;
|
||||
var cv = Cv;
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
NavBack.IsEnabled = cv.CanGoBack;
|
||||
NavForward.IsEnabled = cv.CanGoForward;
|
||||
NavBack.IsEnabled = cv?.CanGoBack == true;
|
||||
NavForward.IsEnabled = cv?.CanGoForward == true;
|
||||
}));
|
||||
}
|
||||
|
||||
// ---- 탭 스트립 UI 이벤트 ----
|
||||
private void NewTab_Click(object sender, RoutedEventArgs e) => _tabs.NewTab(_config.HomeUrl);
|
||||
|
||||
private void Tab_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
_tabs.Activate(tab);
|
||||
}
|
||||
|
||||
private void Tab_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 휠 클릭(미들) → 탭 닫기 (브라우저 관례)
|
||||
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
{
|
||||
_tabs.CloseTab(tab);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TabClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
_tabs.CloseTab(tab);
|
||||
}
|
||||
|
||||
// T1-8 스피커 아이콘 클릭 → 음소거 토글
|
||||
private void TabAudio_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem { Core: { } cv } tab)
|
||||
{
|
||||
try { cv.IsMuted = !cv.IsMuted; tab.IsMuted = cv.IsMuted; } catch { }
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnResponseReceived(object? sender, CoreWebView2WebResourceResponseReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
|
|
@ -273,7 +426,7 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch { }
|
||||
|
||||
string referer = Browser.CoreWebView2?.Source ?? "";
|
||||
string referer = (sender as CoreWebView2)?.Source ?? Cv?.Source ?? "";
|
||||
try { var r = e.Request.Headers.GetHeader("Referer"); if (!string.IsNullOrEmpty(r)) referer = r; } catch { }
|
||||
|
||||
var kind = MediaDetector.Classify(uri, ct, len, _config);
|
||||
|
|
@ -348,7 +501,7 @@ public partial class MainWindow : Window
|
|||
|
||||
private async Task<List<CoreWebView2Cookie>> GetCookiesAsync(Uri url)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) return new();
|
||||
try { return (await cv.CookieManager.GetCookiesAsync(url.ToString())).ToList(); }
|
||||
catch { return new(); }
|
||||
|
|
@ -455,9 +608,9 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
|
||||
private void NavBack_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.GoBack();
|
||||
private void NavForward_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.GoForward();
|
||||
private void NavRefresh_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.Reload();
|
||||
private void NavBack_Click(object sender, RoutedEventArgs e) => Cv?.GoBack();
|
||||
private void NavForward_Click(object sender, RoutedEventArgs e) => Cv?.GoForward();
|
||||
private void NavRefresh_Click(object sender, RoutedEventArgs e) => Cv?.Reload();
|
||||
private void NavHome_Click(object sender, RoutedEventArgs e) => NavigateTo(_config.HomeUrl);
|
||||
|
||||
internal void NavigateTo(string? url)
|
||||
|
|
@ -465,21 +618,21 @@ public partial class MainWindow : Window
|
|||
// T1-4: URL/검색어 판별 + 기본 엔진·키워드 전환(g, yt, ...)은 리졸버가 담당
|
||||
var intent = _resolver.Resolve(url);
|
||||
if (intent == null) return;
|
||||
Browser.CoreWebView2?.Navigate(intent.Target);
|
||||
Cv?.Navigate(intent.Target);
|
||||
}
|
||||
|
||||
// 소셜 영상 페이지(YouTube/Instagram/X/...) 감지 → yt-dlp 카드 등록.
|
||||
private void DetectSocial(string? url)
|
||||
// 소셜 영상 페이지(YouTube/Instagram/X/...) 감지 → yt-dlp 카드 등록. (탭별 상태)
|
||||
private void DetectSocial(TabItem tab, string? url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) { _pendingSocialUrl = null; return; }
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { _pendingSocialUrl = null; return; }
|
||||
if (string.IsNullOrEmpty(url)) { tab.PendingSocialUrl = null; return; }
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { tab.PendingSocialUrl = null; return; }
|
||||
if (SocialSiteDetector.TryMatch(uri, out var normalized, out var label))
|
||||
{
|
||||
_manager.LogPanel($"★ 소셜 감지 [{label}]: {normalized}");
|
||||
_manager.OnDetected(new DetectionContext(normalized, normalized, MediaKind.Social, Tag: label));
|
||||
_pendingSocialUrl = normalized;
|
||||
tab.PendingSocialUrl = normalized;
|
||||
}
|
||||
else _pendingSocialUrl = null;
|
||||
else tab.PendingSocialUrl = null;
|
||||
}
|
||||
|
||||
// ---- 링크로 받기 (URL 붙여넣기) ----
|
||||
|
|
@ -506,27 +659,40 @@ public partial class MainWindow : Window
|
|||
{
|
||||
_manager.LogPanel($"★ 소셜 감지 [{label}]: {normalized}");
|
||||
_manager.OnDetected(new DetectionContext(normalized, normalized, MediaKind.Social, Tag: label));
|
||||
_pendingSocialUrl = normalized;
|
||||
LinkPopup.IsOpen = false;
|
||||
Log("카드에 추가했습니다. 화질을 선택하고 받기를 누르세요");
|
||||
}
|
||||
else Log("지원하는 소셜 영상 링크가 아닙니다 (YouTube·Instagram·X·TikTok·Facebook)");
|
||||
}
|
||||
|
||||
// #7 전역 키보드 단축키 (+T1-3/T1-9/T1-11)
|
||||
// #7 전역 키보드 단축키 (+Phase A 탭/T1-3/T1-9/T1-11)
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
var mods = Keyboard.Modifiers;
|
||||
|
||||
// Phase A 탭 단축키
|
||||
if (mods == ModifierKeys.Control && e.Key == Key.T) { _tabs.NewTab(_config.HomeUrl); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.T) { _tabs.ReopenClosedTab(); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.W) { _tabs.CloseTab(_tabs.ActiveTab!); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.W) { if (_tabs.ActiveTab != null) _tabs.CloseTab(_tabs.ActiveTab); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.Tab) { _tabs.ActivateNext(); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.Tab) { _tabs.ActivatePrevious(); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key >= Key.D1 && e.Key <= Key.D9)
|
||||
{
|
||||
var idx = e.Key - Key.D1;
|
||||
if (idx == 8) _tabs.ActivateLast(); else _tabs.ActivateAt(idx); // Ctrl+9 = 마지막 탭
|
||||
e.Handled = true;
|
||||
}
|
||||
// T1-3 검색 바가 열려 있으면 Esc 는 닫기가 우선
|
||||
if (e.Key == Key.Escape && FindBar.Visibility == Visibility.Visible) { CloseFind(); e.Handled = true; }
|
||||
else if (e.Key == Key.Escape && FindBar.Visibility == Visibility.Visible) { CloseFind(); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.F) { OpenFind(); e.Handled = true; }
|
||||
else if (e.Key == Key.F3)
|
||||
{
|
||||
if (FindBar.Visibility == Visibility.Visible)
|
||||
{
|
||||
if (mods == ModifierKeys.Shift) _findSession?.Previous(); else _findSession?.Next();
|
||||
if (mods == ModifierKeys.Shift) _tabs.ActiveTab?.FindSession?.Previous();
|
||||
else _tabs.ActiveTab?.FindSession?.Next();
|
||||
}
|
||||
else OpenFind();
|
||||
e.Handled = true;
|
||||
|
|
@ -540,66 +706,80 @@ public partial class MainWindow : Window
|
|||
else if (mods == ModifierKeys.Alt && e.Key == Key.Right) { try { if (cv?.CanGoForward == true) cv.GoForward(); } catch { } e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control)
|
||||
{
|
||||
if (e.Key == Key.OemPlus || e.Key == Key.Add) { try { Browser.ZoomFactor = Math.Min(Browser.ZoomFactor + 0.1, 5); PersistZoom(); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.OemMinus || e.Key == Key.Subtract) { try { Browser.ZoomFactor = Math.Max(Browser.ZoomFactor - 0.1, 0.1); PersistZoom(); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.D0) { try { Browser.ZoomFactor = 1.0; PersistZoom(); } catch { } e.Handled = true; }
|
||||
if (e.Key == Key.OemPlus || e.Key == Key.Add) { try { ZoomBy(+0.1); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.OemMinus || e.Key == Key.Subtract) { try { ZoomBy(-0.1); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.D0) { try { ZoomSet(1.0); } catch { } e.Handled = true; }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- T1-3 페이지 내 검색 바 ----
|
||||
private void ZoomBy(double delta) => ZoomSet(Math.Clamp((ActiveView?.ZoomFactor ?? 1.0) + delta, 0.1, 5.0));
|
||||
|
||||
private void ZoomSet(double zoom)
|
||||
{
|
||||
if (ActiveView == null) return;
|
||||
ActiveView.ZoomFactor = zoom;
|
||||
PersistZoom();
|
||||
}
|
||||
|
||||
// ---- T1-3 페이지 내 검색 바 (활성 탭 세션) ----
|
||||
internal void OpenFind()
|
||||
{
|
||||
FindBar.Visibility = Visibility.Visible;
|
||||
FindStatus.Text = "";
|
||||
FindBox.Focus();
|
||||
FindBox.SelectAll();
|
||||
if (FindBox.Text.Length > 0) _ = _findSession?.StartAsync(FindBox.Text);
|
||||
if (FindBox.Text.Length > 0) _ = _tabs.ActiveTab?.FindSession?.StartAsync(FindBox.Text);
|
||||
}
|
||||
|
||||
internal void CloseFind()
|
||||
{
|
||||
FindBar.Visibility = Visibility.Collapsed;
|
||||
_findSession?.Close();
|
||||
_tabs.ActiveTab?.FindSession?.Close();
|
||||
FindStatus.Text = "";
|
||||
}
|
||||
|
||||
private void FindBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (FindBar.Visibility == Visibility.Visible)
|
||||
_ = _findSession?.StartAsync(FindBox.Text);
|
||||
_ = _tabs.ActiveTab?.FindSession?.StartAsync(FindBox.Text);
|
||||
}
|
||||
|
||||
private void FindBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter) { if (Keyboard.Modifiers == ModifierKeys.Shift) _findSession?.Previous(); else _findSession?.Next(); e.Handled = true; }
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
if (Keyboard.Modifiers == ModifierKeys.Shift) _tabs.ActiveTab?.FindSession?.Previous();
|
||||
else _tabs.ActiveTab?.FindSession?.Next();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape) { CloseFind(); e.Handled = true; }
|
||||
}
|
||||
|
||||
private void FindNext_Click(object sender, RoutedEventArgs e) => _findSession?.Next();
|
||||
private void FindPrev_Click(object sender, RoutedEventArgs e) => _findSession?.Previous();
|
||||
private void FindNext_Click(object sender, RoutedEventArgs e) => _tabs.ActiveTab?.FindSession?.Next();
|
||||
private void FindPrev_Click(object sender, RoutedEventArgs e) => _tabs.ActiveTab?.FindSession?.Previous();
|
||||
private void FindClose_Click(object sender, RoutedEventArgs e) => CloseFind();
|
||||
|
||||
private void FindOption_Changed(object sender, RoutedEventArgs e)
|
||||
=> _findSession?.SetOptions(FindCaseBox.IsChecked == true, FindWordBox.IsChecked == true);
|
||||
=> _tabs.ActiveTab?.FindSession?.SetOptions(FindCaseBox.IsChecked == true, FindWordBox.IsChecked == true);
|
||||
|
||||
// T1-7 줌 변경을 사이트별 저장소에 영속
|
||||
private void PersistZoom()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
if (cv != null) _zoomStore.Set(cv.Source, Browser.ZoomFactor);
|
||||
var cv = Cv;
|
||||
if (cv != null && ActiveView != null) _zoomStore.Set(cv.Source, ActiveView.ZoomFactor);
|
||||
}
|
||||
|
||||
// T1-9 강제 새로고침: 디스크 캐시 비우고 다시 로드
|
||||
private async void ForceReload()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) return;
|
||||
try { await cv.Profile.ClearBrowsingDataAsync(CoreWebView2BrowsingDataKinds.DiskCache); }
|
||||
catch { }
|
||||
try { cv.Reload(); } catch { }
|
||||
}
|
||||
|
||||
// T1-11 전체화면(F11): 툴바/상태바 숨기고 최대화. 재누르면 복원.
|
||||
// T1-11 전체화면(F11): 툴바/탭/상태바 숨기고 최대화. 재누르면 복원.
|
||||
internal void ToggleFullscreen()
|
||||
{
|
||||
_fullscreen = !_fullscreen;
|
||||
|
|
@ -613,6 +793,7 @@ public partial class MainWindow : Window
|
|||
WindowState = _stateBeforeFullscreen;
|
||||
}
|
||||
ToolbarRow.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
TabStripRow.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
StatusBar.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
|
|
@ -672,6 +853,8 @@ public partial class MainWindow : Window
|
|||
_config.RightPanelCollapsed = _panelCollapsed;
|
||||
if (!_panelCollapsed) _config.RightPanelWidth = ClampPanelWidth(RightCol.Width.Value);
|
||||
ConfigStore.Save(_config);
|
||||
// T1-10 세션 저장: 탭 URL/활성/핀 보존
|
||||
try { if (_tabs != null) TabSessionStore.Save(SessionPath, _tabs.CaptureSession()); } catch { }
|
||||
}
|
||||
|
||||
// ---- 개인정보 보호 모드 + 브라우저 데이터 관리 ----
|
||||
|
|
@ -707,7 +890,7 @@ public partial class MainWindow : Window
|
|||
/// <summary>현재 프로필의 브라우저 데이터(쿠키·DOM 저장소·캐시) 삭제. 일반 모드: 영속 프로필 정리.</summary>
|
||||
public async Task ClearBrowsingDataAsync()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) { Log("브라우저가 아직 준비되지 않았습니다"); return; }
|
||||
try
|
||||
{
|
||||
|
|
@ -727,7 +910,7 @@ public partial class MainWindow : Window
|
|||
|
||||
private void AddBookmark_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
var url = cv?.Source ?? "";
|
||||
if (string.IsNullOrEmpty(url) || url.StartsWith("about:", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_bookmarks.Any(b => b.Url == url)) { Log("이미 즐겨찾기에 있습니다"); return; }
|
||||
|
|
|
|||
|
|
@ -70,4 +70,7 @@ public class AppConfig
|
|||
|
||||
/// <summary>주소창 기본 검색 엔진(SearchEngines.Presets 의 Name). 검색어 입력 시 이 엔진으로 검색.</summary>
|
||||
public string SearchEngine { get; set; } = "Google";
|
||||
|
||||
/// <summary>종료 시 탭을 저장하고 다음 시작에 복원(T1-10 세션 복원).</summary>
|
||||
public bool RestoreSession { get; set; } = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ using VideoDownloader.Tests.Infrastructure;
|
|||
namespace VideoDownloader.Tests.E2E;
|
||||
|
||||
/// <summary>
|
||||
/// E2E 스모크: 실제 MainWindow 를 기동해 WebView2 초기화·탐색·검색 바·전체화면 전환이
|
||||
/// 크래시 없이 동작하는지 검증한다(Phase 0 통합 확인). 네트워크 의존 제거를 위해 about:blank 사용.
|
||||
/// E2E (Phase A): 실제 MainWindow 기동 → WebView2 초기화 → about:blank 탐색 →
|
||||
/// 다중 탭(생성/전환/닫기/Ctrl+Shift+T 복원) + 검색 바 + 전체화면까지 실 창에서 검증.
|
||||
/// </summary>
|
||||
[Collection("E2E")]
|
||||
public class MainWindowSmokeE2ETests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MainWindow_기동_탐색_검색바_전체화면_스모크()
|
||||
public async Task MainWindow_기동_탐색_탭전환_닫기_복원_검색바_전체화면_E2E()
|
||||
{
|
||||
using var sta = StaDispatcher.Start();
|
||||
WpfTestApp.Ensure(sta.Dispatcher);
|
||||
|
|
@ -21,38 +21,70 @@ public class MainWindowSmokeE2ETests
|
|||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// WebView2 초기화 대기 (Loaded → InitializeBrowserAsync)
|
||||
var cv = await Wait.Until(
|
||||
() => Task.FromResult(window.Browser.CoreWebView2),
|
||||
c => c != null, 30000);
|
||||
// 첫 탭 WebView2 초기화 대기
|
||||
var first = await Wait.Until(
|
||||
() => Task.FromResult(window.Tabs.ActiveTab),
|
||||
t => t?.Core != null, 30000);
|
||||
var cv1 = first.Core!;
|
||||
|
||||
// about:blank 탐색 (네트워크 불필요)
|
||||
window.NavigateTo("about:blank");
|
||||
await Wait.Until(
|
||||
() => Task.FromResult(cv.Source ?? ""),
|
||||
s => s.Contains("about:blank"), 15000);
|
||||
await Wait.Until(() => Task.FromResult(cv1.Source ?? ""), s => s.Contains("about:blank"), 15000);
|
||||
|
||||
// T1-3 검색 바 열기/닫기
|
||||
// ── Phase A: 다중 탭 ──
|
||||
var tab2 = window.Tabs.NewTab();
|
||||
var cv2 = await Wait.Until(() => Task.FromResult(tab2.Core), c => c != null, 30000);
|
||||
cv2.NavigateToString("<html><body><p id='x'>hello-tab2</p></body></html>");
|
||||
await WaitUntilNavCompleted(cv2);
|
||||
|
||||
Assert.NotSame(first, window.Tabs.ActiveTab); // NewTab 이 활성화
|
||||
Assert.Equal(2, window.Tabs.Tabs.Count);
|
||||
|
||||
// 탭 전환: 첫 탭으로
|
||||
window.Tabs.Activate(first);
|
||||
var switched = ReferenceEquals(window.Tabs.ActiveTab, first);
|
||||
|
||||
// 탭 닫기 → 오른쪽 이웃 활성화 → Ctrl+Shift+T 복원
|
||||
window.Tabs.CloseTab(tab2);
|
||||
var countAfterClose = window.Tabs.Tabs.Count;
|
||||
var reopened = window.Tabs.ReopenClosedTab();
|
||||
var countAfterReopen = window.Tabs.Tabs.Count;
|
||||
|
||||
// ── T1-3 검색 바 ──
|
||||
window.OpenFind();
|
||||
var findVisible = window.FindBar.Visibility == Visibility.Visible;
|
||||
window.CloseFind();
|
||||
var findCollapsed = window.FindBar.Visibility == Visibility.Collapsed;
|
||||
|
||||
// T1-11 전체화면 토글
|
||||
// ── T1-11 전체화면 ──
|
||||
window.ToggleFullscreen();
|
||||
var toolbarHidden = window.ToolbarRow.Visibility == Visibility.Collapsed;
|
||||
var toolbarHidden = window.ToolbarRow.Visibility == Visibility.Collapsed
|
||||
&& window.TabStripRow.Visibility == Visibility.Collapsed;
|
||||
window.ToggleFullscreen();
|
||||
var toolbarBack = window.ToolbarRow.Visibility == Visibility.Visible;
|
||||
|
||||
window.Close();
|
||||
return (init: cv != null, nav: true, findVisible, findCollapsed, toolbarHidden, toolbarBack);
|
||||
return (init: true, switched, countAfterClose, reopened: reopened != null, countAfterReopen,
|
||||
findVisible, findCollapsed, toolbarHidden, toolbarBack);
|
||||
});
|
||||
|
||||
Assert.True(ok.init, "WebView2 초기화 실패");
|
||||
Assert.True(ok.nav);
|
||||
Assert.True(ok.switched, "탭 전환 실패");
|
||||
Assert.True(ok.countAfterClose == 1, $"탭 닫기 후 개수 오류: {ok.countAfterClose}");
|
||||
Assert.True(ok.reopened, "닫은 탭 복원 실패");
|
||||
Assert.True(ok.countAfterReopen == 2, $"복원 후 개수 오류: {ok.countAfterReopen}");
|
||||
Assert.True(ok.findVisible, "검색 바 미표시");
|
||||
Assert.True(ok.findCollapsed, "검색 바 미닫힘");
|
||||
Assert.True(ok.toolbarHidden, "전체화면에서 툴바 미숨김");
|
||||
Assert.True(ok.toolbarHidden, "전체화면에서 크롬 미숨김");
|
||||
Assert.True(ok.toolbarBack, "전체화면 복원 실패");
|
||||
}
|
||||
|
||||
private static Task WaitUntilNavCompleted(Microsoft.Web.WebView2.Core.CoreWebView2 cv)
|
||||
{
|
||||
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
EventHandler<Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs>? h = null;
|
||||
h = (_, _) => { cv.NavigationCompleted -= h; tcs.TrySetResult(); };
|
||||
cv.NavigationCompleted += h;
|
||||
return tcs.Task.WaitAsync(TimeSpan.FromSeconds(15));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue