@@ -938,4 +938,313 @@ describe('PopupController second test file', () => {
938938 controller . resetUI ( ) ;
939939 expect ( splitModeContainer . style . display ) . toBe ( 'block' ) ;
940940 } ) ;
941+
942+ it ( 'openInNewContext uses tabs.create when windows API is unavailable and tab is returned' , async ( ) => {
943+ const controller = new PopupController ( ) ;
944+ delete global . browser . windows ;
945+ global . browser . tabs = { create : vi . fn ( async ( ) => ( { id : 5 , active : true } ) ) } ;
946+
947+ const warnSpy = vi . spyOn ( console , 'warn' ) ;
948+ await controller . openInNewContext ( 'popup.html?test=1' ) ;
949+
950+ expect ( global . browser . tabs . create ) . toHaveBeenCalledWith ( { url : 'popup.html?test=1' , active : true } ) ;
951+ expect ( warnSpy ) . not . toHaveBeenCalledWith ( 'Tab created but no ID found:' , expect . anything ( ) ) ;
952+ } ) ;
953+
954+ it ( 'openInNewContext warns when tabs.create returns null' , async ( ) => {
955+ const controller = new PopupController ( ) ;
956+ delete global . browser . windows ;
957+ global . browser . tabs = { create : vi . fn ( async ( ) => null ) } ;
958+
959+ const warnSpy = vi . spyOn ( console , 'warn' ) ;
960+ await controller . openInNewContext ( 'popup.html?test=1' ) ;
961+
962+ expect ( warnSpy ) . toHaveBeenCalledWith ( 'Tab created but no ID found:' , null ) ;
963+ } ) ;
964+
965+ it ( 'openInNewContext logs error when neither windows nor tabs API is available' , async ( ) => {
966+ const controller = new PopupController ( ) ;
967+ delete global . browser . windows ;
968+ delete global . browser . tabs ;
969+
970+ const errorSpy = vi . spyOn ( console , 'error' ) ;
971+ await controller . openInNewContext ( 'popup.html?test=1' ) ;
972+
973+ expect ( errorSpy ) . toHaveBeenCalledWith ( 'No window/tab API available' ) ;
974+ } ) ;
975+
976+ it ( '_setupTranslatorSelector returns null when translatorContainer is missing from DOM' , ( ) => {
977+ const controller = new PopupController ( ) ;
978+ const tc = document . getElementById ( 'translatorContainer' ) ;
979+ if ( tc ) tc . parentNode . removeChild ( tc ) ;
980+
981+ const result = controller . _setupTranslatorSelector ( [ { branches : [ { branch_id : 1 } ] } ] , null ) ;
982+ expect ( result ) . toBeNull ( ) ;
983+ } ) ;
984+
985+ it ( '_setupTranslatorSelector hides container and returns null when chapters have no branches' , ( ) => {
986+ const controller = new PopupController ( ) ;
987+ const tc = document . getElementById ( 'translatorContainer' ) ;
988+ tc . style . display = 'block' ;
989+
990+ const result = controller . _setupTranslatorSelector ( [ { volume : 1 } , { volume : 2 } ] , null ) ;
991+
992+ expect ( tc . style . display ) . toBe ( 'none' ) ;
993+ expect ( result ) . toBeNull ( ) ;
994+ } ) ;
995+
996+ it ( '_setupTranslatorSelector hides container and returns branch id for single branch' , ( ) => {
997+ const controller = new PopupController ( ) ;
998+ const tc = document . getElementById ( 'translatorContainer' ) ;
999+ tc . style . display = 'block' ;
1000+
1001+ const result = controller . _setupTranslatorSelector ( [
1002+ { branches : [ { branch_id : 5 , teams : [ { name : 'Solo Team' } ] } ] }
1003+ ] , null ) ;
1004+
1005+ expect ( tc . style . display ) . toBe ( 'none' ) ;
1006+ expect ( result ) . toBe ( 5 ) ;
1007+ } ) ;
1008+
1009+ it ( '_setupTranslatorSelector shows selector with team names when multiple branches exist' , ( ) => {
1010+ const controller = new PopupController ( ) ;
1011+ const chapters = [
1012+ { branches : [ { branch_id : 1 , teams : [ { name : 'Team A' } ] } , { branch_id : 2 , teams : [ { name : 'Team B' } ] } ] } ,
1013+ { /* no branches */ }
1014+ ] ;
1015+
1016+ const result = controller . _setupTranslatorSelector ( chapters , null ) ;
1017+
1018+ const tc = document . getElementById ( 'translatorContainer' ) ;
1019+ expect ( tc . style . display ) . toBe ( 'block' ) ;
1020+ const ts = document . getElementById ( 'translatorSelect' ) ;
1021+ expect ( ts . options . length ) . toBe ( 2 ) ;
1022+ expect ( ts . options [ 0 ] . textContent ) . toBe ( 'Team A' ) ;
1023+ expect ( ts . options [ 1 ] . textContent ) . toBe ( 'Team B' ) ;
1024+ expect ( result ) . toBe ( 1 ) ;
1025+ } ) ;
1026+
1027+ it ( '_setupTranslatorSelector uses default name when branch has no teams' , ( ) => {
1028+ const controller = new PopupController ( ) ;
1029+ const chapters = [
1030+ { branches : [ { branch_id : 10 } , { branch_id : 20 } ] }
1031+ ] ;
1032+
1033+ controller . _setupTranslatorSelector ( chapters , null ) ;
1034+
1035+ const ts = document . getElementById ( 'translatorSelect' ) ;
1036+ expect ( ts . options [ 0 ] . textContent ) . toBe ( 'Перевод 10' ) ;
1037+ expect ( ts . options [ 1 ] . textContent ) . toBe ( 'Перевод 20' ) ;
1038+ } ) ;
1039+
1040+ it ( '_setupTranslatorSelector uses branchIdFromUrl when it matches a known branch' , ( ) => {
1041+ const controller = new PopupController ( ) ;
1042+ const chapters = [
1043+ { branches : [ { branch_id : 1 , teams : [ { name : 'Team A' } ] } , { branch_id : 2 , teams : [ { name : 'Team B' } ] } ] }
1044+ ] ;
1045+
1046+ const result = controller . _setupTranslatorSelector ( chapters , '2' ) ;
1047+
1048+ expect ( result ) . toBe ( 2 ) ;
1049+ const ts = document . getElementById ( 'translatorSelect' ) ;
1050+ expect ( Number ( ts . value ) ) . toBe ( 2 ) ;
1051+ } ) ;
1052+
1053+ it ( '_setupTranslatorSelector onchange repopulates chapter selects with filtered chapters' , ( ) => {
1054+ const controller = new PopupController ( ) ;
1055+ const chapters = [
1056+ { volume : 1 , number : 1 , branches : [ { branch_id : 1 , teams : [ { name : 'A' } ] } , { branch_id : 2 , teams : [ { name : 'B' } ] } ] } ,
1057+ { volume : 1 , number : 2 , branches : [ { branch_id : 2 , teams : [ { name : 'B' } ] } ] }
1058+ ] ;
1059+ controller . _allChapters = chapters ;
1060+
1061+ controller . _setupTranslatorSelector ( chapters , null ) ;
1062+
1063+ const repopulateSpy = vi . spyOn ( controller , '_repopulateChapterSelects' ) ;
1064+ const ts = document . getElementById ( 'translatorSelect' ) ;
1065+ ts . value = '2' ;
1066+ ts . dispatchEvent ( new Event ( 'change' ) ) ;
1067+
1068+ expect ( repopulateSpy ) . toHaveBeenCalled ( ) ;
1069+ } ) ;
1070+
1071+ it ( '_setupTranslatorSelector onchange skips repopulate when chapter selects are removed from DOM' , ( ) => {
1072+ const controller = new PopupController ( ) ;
1073+ const chapters = [
1074+ { volume : 1 , number : 1 , branches : [ { branch_id : 1 , teams : [ { name : 'A' } ] } , { branch_id : 2 , teams : [ { name : 'B' } ] } ] }
1075+ ] ;
1076+ controller . _allChapters = chapters ;
1077+
1078+ controller . _setupTranslatorSelector ( chapters , null ) ;
1079+
1080+ const fromSelect = document . getElementById ( 'chapterFromSelect' ) ;
1081+ if ( fromSelect ) fromSelect . parentNode . removeChild ( fromSelect ) ;
1082+
1083+ const repopulateSpy = vi . spyOn ( controller , '_repopulateChapterSelects' ) ;
1084+ const ts = document . getElementById ( 'translatorSelect' ) ;
1085+ ts . value = '2' ;
1086+ ts . dispatchEvent ( new Event ( 'change' ) ) ;
1087+
1088+ expect ( repopulateSpy ) . not . toHaveBeenCalled ( ) ;
1089+ } ) ;
1090+
1091+ it ( '_getFilteredChapters returns only chapters matching the given branchId' , ( ) => {
1092+ const controller = new PopupController ( ) ;
1093+ controller . _allChapters = [
1094+ { volume : 1 , number : 1 , branches : [ { branch_id : 1 } ] } ,
1095+ { volume : 1 , number : 2 , branches : [ { branch_id : 2 } ] } ,
1096+ { volume : 1 , number : 3 , branches : [ { branch_id : 1 } , { branch_id : 2 } ] } ,
1097+ { volume : 2 , number : 1 }
1098+ ] ;
1099+
1100+ const result = controller . _getFilteredChapters ( 1 ) ;
1101+
1102+ expect ( result ) . toHaveLength ( 2 ) ;
1103+ expect ( result [ 0 ] . number ) . toBe ( 1 ) ;
1104+ expect ( result [ 1 ] . number ) . toBe ( 3 ) ;
1105+ } ) ;
1106+
1107+ it ( 'Appends branchId to URL params when translatorContainer is visible during download button click' , async ( ) => {
1108+ const controller = new PopupController ( ) ;
1109+ controller . isInSeparateWindow = vi . fn ( ) . mockResolvedValue ( false ) ;
1110+ controller . loadedFile = null ;
1111+
1112+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1113+
1114+ const tc = document . getElementById ( 'translatorContainer' ) ;
1115+ tc . style . display = 'block' ;
1116+ const ts = document . getElementById ( 'translatorSelect' ) ;
1117+ const opt = document . createElement ( 'option' ) ;
1118+ opt . value = '42' ;
1119+ ts . appendChild ( opt ) ;
1120+ ts . value = '42' ;
1121+
1122+ const windowsCreateSpy = vi . spyOn ( global . browser . windows , 'create' ) . mockResolvedValue ( { id : 123 } ) ;
1123+
1124+ document . getElementById ( 'downloadBtn' ) . click ( ) ;
1125+
1126+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1127+
1128+ const urlArg = windowsCreateSpy . mock . calls [ 0 ] ?. [ 0 ] ?. url ;
1129+ expect ( urlArg ) . toContain ( 'branchId=42' ) ;
1130+ } ) ;
1131+
1132+ it ( 'Warns when translatorContainer is missing during download start' , async ( ) => {
1133+ const controller = new PopupController ( ) ;
1134+ controller . currentSlug = 'slug' ;
1135+ controller . currentServiceKey = 'ranobelib' ;
1136+
1137+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1138+
1139+ const tc = document . getElementById ( 'translatorContainer' ) ;
1140+ if ( tc ) tc . parentNode . removeChild ( tc ) ;
1141+
1142+ const warnSpy = vi . spyOn ( console , 'warn' ) ;
1143+ await controller . startDownload ( ) ;
1144+
1145+ expect ( warnSpy ) . toHaveBeenCalledWith ( 'Translator container not found when hiding during download' ) ;
1146+ } ) ;
1147+
1148+ it ( 'Warns when translatorContainer is missing during UI reset' , ( ) => {
1149+ const controller = new PopupController ( ) ;
1150+
1151+ const tc = document . getElementById ( 'translatorContainer' ) ;
1152+ if ( tc ) tc . parentNode . removeChild ( tc ) ;
1153+
1154+ const warnSpy = vi . spyOn ( console , 'warn' ) ;
1155+ controller . resetUI ( ) ;
1156+
1157+ expect ( warnSpy ) . toHaveBeenCalledWith ( 'Translator container not found when resetting UI' ) ;
1158+ } ) ;
1159+
1160+ it ( '_loadChaptersAndPopulateSelects skips hiding translatorContainer when it is absent and no multiple branches' , async ( ) => {
1161+ const controller = new PopupController ( ) ;
1162+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1163+
1164+ const tc = document . getElementById ( 'translatorContainer' ) ;
1165+ if ( tc ) tc . parentNode . removeChild ( tc ) ;
1166+
1167+ const service = { fetchChaptersList : vi . fn ( async ( ) => ( { data : [ { volume : 1 , number : 1 } ] } ) ) } ;
1168+ const result = await controller . _loadChaptersAndPopulateSelects ( service , 'slug' , null , null ) ;
1169+
1170+ expect ( result ) . toBe ( 1 ) ;
1171+ } ) ;
1172+
1173+ it ( '_loadChaptersAndPopulateSelects calls _setupTranslatorSelector and filters chapters when multiple branches exist' , async ( ) => {
1174+ const controller = new PopupController ( ) ;
1175+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1176+
1177+ const chapters = [
1178+ { volume : 1 , number : 1 , branches : [
1179+ { branch_id : 1 , teams : [ { name : 'Team A' } ] } ,
1180+ { branch_id : 2 , teams : [ { name : 'Team B' } ] }
1181+ ] } ,
1182+ { volume : 1 , number : 2 , branches : [ { branch_id : 1 , teams : [ { name : 'Team A' } ] } ] }
1183+ ] ;
1184+ const service = { fetchChaptersList : vi . fn ( async ( ) => ( { data : chapters } ) ) } ;
1185+
1186+ const count = await controller . _loadChaptersAndPopulateSelects ( service , 'slug' , null , null ) ;
1187+
1188+ expect ( count ) . toBe ( 2 ) ;
1189+ const tc = document . getElementById ( 'translatorContainer' ) ;
1190+ expect ( tc . style . display ) . toBe ( 'block' ) ;
1191+ } ) ;
1192+
1193+ it ( '_renderMeta handles empty authors array (covers false branches of secondLine ternaries)' , async ( ) => {
1194+ global . serviceRegistry . getServiceByUrl = vi . fn ( ( ) => ( {
1195+ name : 'ranobelib' ,
1196+ fetchMangaMetadata : vi . fn ( async ( ) => ( {
1197+ data : {
1198+ rus_name : 'Title' , summary : 'Summary' , cover : 'cover.png' ,
1199+ authors : [ ] , artists : [ ] ,
1200+ ageRestriction : { label : '18+' } , releaseDate : '2020'
1201+ }
1202+ } ) ) ,
1203+ fetchChaptersList : vi . fn ( async ( ) => ( { data : [ ] } ) )
1204+ } ) ) ;
1205+
1206+ const controller = new PopupController ( ) ;
1207+ await controller . loadMetadata ( ) ;
1208+
1209+ const logoInfo = document . getElementById ( 'logoInfo' ) ;
1210+ expect ( logoInfo . textContent ) . not . toContain ( 'Авторы:' ) ;
1211+ } ) ;
1212+
1213+ it ( 'loadMetadata parses integer branchId from URL params (covers true branch at branchIdFromUrl)' , async ( ) => {
1214+ Object . defineProperty ( window , 'location' , {
1215+ value : { search : '?branchId=2' } ,
1216+ configurable : true
1217+ } ) ;
1218+
1219+ const controller = new PopupController ( ) ;
1220+ await controller . loadMetadata ( ) ;
1221+
1222+ expect ( document . getElementById ( 'downloadBtn' ) . disabled ) . toBe ( false ) ;
1223+ } ) ;
1224+
1225+ it ( 'startDownload passes branchId to downloadManager when translatorContainer is visible' , async ( ) => {
1226+ const controller = new PopupController ( ) ;
1227+ controller . currentSlug = 'slug' ;
1228+ controller . currentServiceKey = 'ranobelib' ;
1229+
1230+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
1231+
1232+ const tc = document . getElementById ( 'translatorContainer' ) ;
1233+ tc . style . display = 'block' ;
1234+ const ts = document . getElementById ( 'translatorSelect' ) ;
1235+ const opt = document . createElement ( 'option' ) ;
1236+ opt . value = '7' ;
1237+ ts . appendChild ( opt ) ;
1238+ ts . value = '7' ;
1239+
1240+ let capturedBranchId ;
1241+ controller . downloadManager . startDownload = vi . fn ( async ( opts ) => {
1242+ capturedBranchId = opts . branchId ;
1243+ return { } ;
1244+ } ) ;
1245+
1246+ await controller . startDownload ( ) ;
1247+
1248+ expect ( capturedBranchId ) . toBe ( 7 ) ;
1249+ } ) ;
9411250} ) ;
0 commit comments