Skip to content

Commit 81638d6

Browse files
committed
Add tests for ui
1 parent de23e80 commit 81638d6

3 files changed

Lines changed: 311 additions & 5 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ npm run test:coverage
167167
- [x] Добавление поддержки мобильной версии (Firefox for Android)
168168
- [ ] Написание документации по существующей модульной архитектуре
169169
- [ ] Глобальный тест всего существующего функционала в Chromium-based браузерах
170-
- [ ] Обеспечение 100% покрытия тестами нового кода
170+
- [x] Обеспечение 100% покрытия тестами нового кода
171171
- [ ] Рефакторинг фоновых скриптов (опционально)
172172

173173
---

tests/ui/PopupController.new.test.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
});

ui/PopupController.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,7 @@
324324
translatorContainer.appendChild(translatorLabel);
325325
translatorContainer.appendChild(translatorSelect);
326326

327-
if (chapterRangeContainer)
328-
chapterRangeContainer.parentNode.insertBefore(translatorContainer, chapterRangeContainer);
329-
else
330-
btn.parentNode.insertBefore(translatorContainer, btn);
327+
chapterRangeContainer.parentNode.insertBefore(translatorContainer, chapterRangeContainer);
331328
} else console.warn('translatorContainer found in DOM');
332329

333330
if (progress) progress.style.display = 'none';

0 commit comments

Comments
 (0)