const fs = require('fs'), assert = require('node:assert/strict'), { createHash } = require('crypto'); const { chromium, expect } = require('@playwright/test'); (async () => { const phase = process.argv[2], portal = JSON.parse(fs.readFileSync('tmp/portal195-private.json')); const state = JSON.parse(fs.readFileSync('tmp/state239-public.json')); const browser = await chromium.launch({ channel: 'msedge', headless: true }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }), errors = [], receipts = []; page.on('pageerror', e => errors.push(e.message)); page.on('response', r => { if (new URL(r.url()).pathname.startsWith('/api/backups') && r.request().method() !== 'GET') receipts.push({ method: r.request().method(), path: new URL(r.url()).pathname, status: r.status() }); }); await page.goto('http://localhost:3000/backups'); await page.getByLabel('Email', { exact: true }).fill(portal.email); await page.getByLabel('Пароль', { exact: true }).fill(portal.password); await page.getByRole('button', { name: 'Войти', exact: true }).click(); await expect(page.getByRole('heading', { name: 'Резервные копии', exact: true })).toBeVisible({ timeout: 30000 }); if (phase === 'configure') { const s = JSON.parse(fs.readFileSync('tmp/s3-ui239-private.json')); const paused = await page.evaluate(async () => { const r = await fetch('/api/backups/schedule', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('mmapp_token') }, body: JSON.stringify({ enabled: false }) }); return { status: r.status, body: await r.json() }; }); assert.equal(paused.status, 200); assert.equal(paused.body.enabled, false); await page.getByRole('tab', { name: 'Настройки', exact: true }).click(); await page.getByRole('button', { name: 'S3', exact: true }).click(); for (const [label, value] of Object.entries({ Endpoint: s.endpoint, Region: s.region, Bucket: s.bucket, Prefix: state.prefix, 'Access key': s.accessKey, 'Secret key': s.secretKey })) await page.getByLabel(label, { exact: true }).fill(value); const path = page.getByRole('switch', { name: 'Path-style', exact: true }); if (await path.getAttribute('aria-checked') === 'true') await path.click(); const saved = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage' && r.request().method() === 'PUT'); await page.getByRole('button', { name: 'Сохранить хранилище', exact: true }).click(); assert.equal((await saved).status(), 200); await expect(page.getByLabel('Secret key', { exact: true })).toHaveValue(''); const tested = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage/test' && r.request().method() === 'POST'); await page.getByRole('button', { name: 'Проверить', exact: true }).click(); assert.equal((await tested).status(), 200); } else { await page.getByRole('button', { name: 'Обновить', exact: true }).click(); await expect(page.getByText(state.filename, { exact: true })).toBeVisible(); if (phase === 'restore') { const download = page.waitForEvent('download'); await page.getByRole('button', { name: 'Скачать ' + state.filename, exact: true }).click(); const d = await download; assert.equal(d.suggestedFilename(), state.filename); const bytes = fs.readFileSync(await d.path()); const expected = Buffer.from(`# Own limited UI restore 239\n/interface bridge add name=${state.bridge} comment=mm-ui239\n`); assert.deepEqual(bytes, expected); await page.getByRole('button', { name: 'Восстановить ' + state.filename, exact: true }).click(); const dialog = page.getByRole('alertdialog'); await expect(dialog).toContainText(state.filename); const restored = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id + '/restore' && r.request().method() === 'POST'); await dialog.getByRole('button', { name: 'Восстановить', exact: true }).click(); const r = await restored; assert.equal(r.status(), 200); const result = await r.json(); assert.equal(result.backupId, state.id); assert.equal(result.serverId, state.serverId); await expect(dialog).toBeHidden(); fs.writeFileSync('docs/ux-ui/acceptance/s3-ui239-browser.json', JSON.stringify({ date: '2026-10-14', block: '12.5.239', realPortalLogin: true, noMocks: true, passed: true, downloadBytes: bytes.length, downloadSha256: createHash('sha256').update(bytes).digest('hex'), restoreStatus: 200, restoreReceiptMatched: true, receipts, pageErrors: errors, fullConfigurationApplied: false }, null, 2) + '\n'); } else if (phase === 'delete') { await page.getByRole('button', { name: 'Удалить ' + state.filename, exact: true }).click(); const deleted = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id && r.request().method() === 'DELETE'); await page.getByRole('alertdialog').getByRole('button', { name: 'Удалить', exact: true }).click(); assert.equal((await deleted).status(), 204); await expect(page.getByRole('alertdialog')).toBeHidden(); await expect(page.getByText(state.filename, { exact: true })).toHaveCount(0); const p = 'docs/ux-ui/acceptance/s3-ui239-browser.json', report = JSON.parse(fs.readFileSync(p)); Object.assign(report, { deleteStatus: 204, ownRowRemovedThroughUi: true, deleteReceipts: receipts }); fs.writeFileSync(p, JSON.stringify(report, null, 2) + '\n'); } else throw Error('Unsupported phase'); } assert.deepEqual(errors, []); console.log('S3 UI239 ' + phase + ' passed'); } finally { await browser.close(); } })().catch(e => { console.error(e.message); process.exitCode = 1; });