Files
ihaleist/tests/browser-acceptance.mjs
T

164 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Local-only browser acceptance runner. It uses the disposable fixtures served by browser-qa.mjs.
import { spawn } from 'node:child_process';
import { writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { chromium } from 'playwright';
const server = spawn(process.execPath, ['tests/browser-qa.mjs'], { cwd: new URL('..', import.meta.url), stdio: ['ignore', 'pipe', 'inherit'] });
let output = '';
const url = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('QA sunucusu başlatılamadı.')), 30_000);
server.stdout.on('data', chunk => {
output += chunk;
const match = /QA_URL=(\S+)/.exec(output);
if (match) { clearTimeout(timeout); resolve(match[1]); }
});
server.once('exit', code => { clearTimeout(timeout); reject(new Error(`QA sunucusu ${code} koduyla kapandı.`)); });
});
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ timezoneId: 'America/New_York' });
const page = await context.newPage();
await page.setViewportSize({ width: 1440, height: 1000 });
const evidence = { url, checks: { browserTimezone: 'America/New_York' }, scope: 'Yerel sentetik QA; gerçek başvuru veya dış sağlayıcı çağrısı yapılmadı.' };
try {
const response = await page.goto(url, { waitUntil: 'networkidle' });
evidence.checks.signInStatus = response?.status();
await page.getByRole('button', { name: 'Test hesabıyla gir' }).click();
await page.waitForLoadState('networkidle');
evidence.checks.visibleCities = await Promise.all(['Giresun', 'Kayseri', 'Akdeniz', 'Aksaray', 'Tepecik'].map(async city => ({ city, count: await page.getByText(city, { exact: false }).count() })));
if (evidence.checks.visibleCities.some(item => item.count < 1)) throw new Error('Beş sentetik şehirden biri görünmedi.');
await page.getByRole('button', { name: 'Günlük Bülten', exact: true }).click();
await page.getByRole('heading', { name: 'Günlük İhale Bülteni', exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.dailyBulletinHeading = await page.getByRole('heading', { name: 'Günlük İhale Bülteni', exact: true }).count() === 1;
if (!evidence.checks.dailyBulletinHeading) throw new Error('Günlük bülten başlığı açılmadı.');
const calendarPage = await context.newPage();
try {
await calendarPage.goto(`${url.replace(/\/qa-signin$/, '')}/qa-calendar-links`, { waitUntil: 'networkidle' });
const googleLink = calendarPage.getByRole('link', { name: 'Google Takvim taslağı', exact: true });
const outlookLink = calendarPage.getByRole('link', { name: 'Outlook taslağı', exact: true });
await googleLink.waitFor({ state: 'attached', timeout: 10_000 });
await outlookLink.waitFor({ state: 'attached', timeout: 10_000 });
const [googleHref, outlookHref] = await Promise.all([googleLink.getAttribute('href'), outlookLink.getAttribute('href')]);
const googleUrl = new URL(googleHref);
const outlookUrl = new URL(outlookHref);
evidence.checks.calendarProviderLinks = {
googleHost: googleUrl.hostname,
googleDates: googleUrl.searchParams.get('dates'),
outlookHost: outlookUrl.hostname,
outlookStart: outlookUrl.searchParams.get('startdt'),
outlookEnd: outlookUrl.searchParams.get('enddt'),
};
if (googleUrl.hostname !== 'calendar.google.com' || googleUrl.searchParams.get('dates') !== '20260910T070000Z/20260910T070000Z' ||
outlookUrl.hostname !== 'outlook.live.com' || outlookUrl.searchParams.get('startdt') !== '2026-09-10T07:00:00Z' ||
outlookUrl.searchParams.get('enddt') !== '2026-09-10T07:00:00Z') {
throw new Error(`Takvim provider bağlantısı kaynak son tarihi korumuyor: ${JSON.stringify(evidence.checks.calendarProviderLinks)}`);
}
} finally { await calendarPage.close(); }
await page.getByRole('button', { name: 'Ana İhale Listesi', exact: true }).click();
await page.getByRole('button', { name: 'Pazar', exact: true }).click();
await page.getByRole('button', { name: 'Analitik Dashboard', exact: true }).click();
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.analyticsTargetLabel = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).count() === 1;
if (!evidence.checks.analyticsTargetLabel) throw new Error('Analitik ciro hedefi alanı erişilebilir ad taşımıyor.');
const primaryTarget = page.getByLabel('Ciro hedefi (TL)', { exact: true });
await primaryTarget.fill('1250');
await page.waitForFunction(() => localStorage.getItem('ihaleist_ciro_hedefi:browser-qa%40example.test') === '1250');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=other`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Pazar', exact: true }).click();
await page.getByRole('button', { name: 'Analitik Dashboard', exact: true }).click();
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.accountScopedAnalyticsTarget = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).inputValue() === '';
if (!evidence.checks.accountScopedAnalyticsTarget) throw new Error('İkinci hesap ilk hesabın ciro hedefini devraldı.');
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).fill('980');
await page.waitForFunction(() => localStorage.getItem('ihaleist_ciro_hedefi:browser-qa-other%40example.test') === '980');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=primary`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Pazar', exact: true }).click();
await page.getByRole('button', { name: 'Analitik Dashboard', exact: true }).click();
await page.getByRole('button', { name: 'Ciro hedefini düzenle', exact: true }).click();
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.primaryAnalyticsTargetRestored = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).inputValue() === '1250';
if (!evidence.checks.primaryAnalyticsTargetRestored) throw new Error('İlk hesabın ciro hedefi hesap değişiminden sonra geri yüklenmedi.');
await page.getByRole('button', { name: 'Keşfet', exact: true }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
await page.waitForLoadState('networkidle');
const icsDir = await mkdtemp(join(tmpdir(), 'ihaleist-ics-'));
const icsPath = join(icsDir, 'qa.ics');
await writeFile(icsPath, 'BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:qa-calendar\r\nDTSTART:20260910T070000Z\r\nSUMMARY:Harici Mikroskop\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n');
try {
await page.locator('input[aria-label="ICS takvim dosyası seç"]').setInputFiles(icsPath);
await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).waitFor();
evidence.checks.icsImported = true;
evidence.checks.icsStorageScoped = await page.evaluate(() => Object.keys(sessionStorage).some(key => key.startsWith('ihaleist_imported_calendar:')));
if (!evidence.checks.icsStorageScoped) throw new Error('ICS kaydı hesap kapsamlı sessionStorage anahtarı kullanmıyor.');
await page.getByRole('button', { name: 'Keşfet' }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
evidence.checks.icsPersistsAfterTabChange = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 1;
if (!evidence.checks.icsPersistsAfterTabChange) throw new Error('ICS kaydı sekme değişiminden sonra korunmadı.');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=other`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Keşfet' }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
evidence.checks.accountScopedIcs = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 0;
if (!evidence.checks.accountScopedIcs) throw new Error('İkinci hesap ilk hesabın ICS etkinliğini devraldı.');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=primary`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Keşfet' }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.primaryIcsRestored = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 1;
if (!evidence.checks.primaryIcsRestored) {
const storage = await page.evaluate(() => Object.fromEntries(Object.keys(sessionStorage).map(key => [key, sessionStorage.getItem(key)])));
throw new Error(`İlk hesabın ICS etkinliği hesap değişiminden sonra geri yüklenmedi: ${JSON.stringify(storage)}`);
}
} finally { await rm(icsDir, { recursive: true, force: true }); }
const readyPage = await context.newPage();
await readyPage.setViewportSize({ width: 1440, height: 1000 });
try {
const readyResponse = await readyPage.goto(`${url.replace(/\/qa-signin$/, '')}/qa-source-update?ready=1&file=1`, { waitUntil: 'networkidle' });
evidence.checks.readyPreparationStatus = readyResponse?.status();
const outputButtons = readyPage.getByRole('button').filter({ hasText: /Teklif Mektubu|Birim Fiyat Teklif Cetveli|Excel Teklif Cetveli|Evrak Kontrol Listesi/ });
evidence.checks.readyOutputButtons = await outputButtons.count();
evidence.checks.readyOutputButtonsEnabled = await outputButtons.evaluateAll(buttons => buttons.every(button => !button.disabled));
if (evidence.checks.readyOutputButtons !== 4 || !evidence.checks.readyOutputButtonsEnabled) throw new Error('Hazır taslak çıktı düğmeleri etkin değil.');
const popupPromise = readyPage.waitForEvent('popup');
await readyPage.getByRole('button', { name: /Evrak Kontrol Listesi/ }).click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
evidence.checks.printPopupTitle = await popup.title();
if (evidence.checks.printPopupTitle !== 'Evrak Kontrol Listesi') throw new Error('Yazdırma penceresi beklenen başlığı üretmedi.');
evidence.checks.printPopupTurkishContent = await popup.locator('h1').filter({ hasText: 'EVRAK KONTROL LİSTESİ' }).count() === 1;
evidence.checks.printPopupChecklistItems = await popup.locator('ul li').count();
if (!evidence.checks.printPopupTurkishContent || evidence.checks.printPopupChecklistItems < 1) throw new Error('Evrak çıktısı beklenen Türkçe başlık veya kontrol listesini içermiyor.');
await popup.emulateMedia({ media: 'print' });
evidence.checks.printButtonHidden = await popup.locator('.yazdirma').evaluate(container => getComputedStyle(container).display === 'none');
if (!evidence.checks.printButtonHidden) throw new Error('Yazdırma düğmesi print ortamında gizlenmedi.');
await popup.close();
} finally { await readyPage.close(); }
const overview = await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-overview`, { waitUntil: 'networkidle' });
evidence.checks.overviewStatus = overview?.status();
evidence.checks.overviewFrames = await page.locator('iframe[title^="Hazırlık"]').count();
if (evidence.checks.overviewFrames !== 3) throw new Error('Hazırlık özeti üç görünüm alanında açılmadı.');
const mobile = await context.newPage();
await mobile.setViewportSize({ width: 390, height: 844 });
try {
const mobileResponse = await mobile.goto(url, { waitUntil: 'networkidle' });
evidence.checks.mobileStatus = mobileResponse?.status();
const mobileOverflow = await mobile.evaluate(() => ({
viewport: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth + 1,
}));
evidence.checks.mobileOverflow = mobileOverflow;
if (mobileOverflow.horizontalOverflow) throw new Error('390px görünümünde yatay taşma var.');
await mobile.getByRole('button', { name: 'Test hesabıyla gir' }).click();
await mobile.waitForLoadState('networkidle');
evidence.checks.mobileTenderList = await mobile.getByLabel('İhale listesi').count() === 1;
if (!evidence.checks.mobileTenderList) throw new Error('390px görünümünde ihale listesi bulunamadı.');
} finally { await mobile.close(); }
} finally {
await page.close(); await context.close(); await browser.close(); server.kill('SIGTERM');
}
await mkdir('docs/quality/evidence', { recursive: true });
await writeFile('docs/quality/evidence/qa-browser-acceptance-runtime.json', JSON.stringify(evidence, null, 2) + '\n');
console.log(JSON.stringify(evidence));