Add opt-in monthly email recaps with scheduling and delivery history
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// Fixture-only browser review. All API calls are intercepted; no real messages are sent.
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { chromium } = require(process.env.REVIEW_PLAYWRIGHT || 'playwright')
|
||||
const base = process.env.REVIEW_BASE || 'http://localhost:3114'
|
||||
const output = process.env.REVIEW_DIR
|
||||
const preview = JSON.parse(fs.readFileSync(process.env.REVIEW_EMAIL_FIXTURE, 'utf8'))
|
||||
|
||||
;(async () => {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
try {
|
||||
const context = await browser.newContext()
|
||||
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }])
|
||||
const calls = []
|
||||
let role = 'admin'
|
||||
let failPreview = false
|
||||
let email = 'viewer@example.test'
|
||||
let settings = { enabled: false, day: 2, hour: 9, public_url: 'https://beta.example.test', next_send_at: null }
|
||||
let preference = { state: 'off', email, can_subscribe: true, detail: 'Ready', schedule_enabled: false, day: 2, hour: 9, next_send_at: null, resend_after: null }
|
||||
let deliveries = []
|
||||
let tokenState = 'ready'
|
||||
const months = Array.from({ length: 23 }, (_, index) => new Date(Date.UTC(2026, 7 - index, 1)).toISOString().slice(0, 7))
|
||||
await context.route('**/api/**', async (route) => {
|
||||
const request = route.request()
|
||||
const url = new URL(request.url())
|
||||
const payload = request.postDataJSON()
|
||||
calls.push({ method: request.method(), path: url.pathname, payload })
|
||||
if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture viewer', role, email } })
|
||||
if (url.pathname === '/api/auth/profile') return route.fulfill({ json: { user: { username: 'Fixture viewer', role, email, auth_provider: 'jellyfin', password_change_supported: true, password_provider: 'jellyfin' }, activity: { recent: [] } } })
|
||||
if (url.pathname === '/api/auth/profile/email') { email = payload.email; preference = { ...preference, email, state: 'off' }; return route.fulfill({ json: { email } }) }
|
||||
if (url.pathname === '/api/profile/email-recaps') {
|
||||
if (request.method() === 'PUT') preference = { ...preference, state: payload.enabled ? 'pending' : 'off', resend_after: payload.enabled ? Date.now() / 1000 + 300 : null }
|
||||
return route.fulfill({ json: preference })
|
||||
}
|
||||
if (url.pathname.startsWith('/api/admin/email-recaps')) {
|
||||
if (role !== 'admin') return route.fulfill({ status: role === 'unauthorized' ? 401 : 403, json: { detail: 'Administrator access is required.' } })
|
||||
if (url.pathname.endsWith('/preview')) {
|
||||
if (failPreview) return route.fulfill({ status: 502, json: { detail: 'Your report is temporarily unavailable. Please try again shortly.' } })
|
||||
return route.fulfill({ json: { ...preview, month: url.searchParams.get('month') } })
|
||||
}
|
||||
if (url.pathname.endsWith('/test')) {
|
||||
assert.deepEqual(Object.keys(payload).sort(), ['month', 'request_id'])
|
||||
deliveries = [{ id: payload.request_id, month: payload.month, kind: 'test', email, username: 'Fixture viewer', state: 'queued', attempts: 0, created_at: Date.now() / 1000, updated_at: Date.now() / 1000, next_attempt_at: Date.now() / 1000, detail: '' }]
|
||||
return route.fulfill({ status: 202, json: { id: payload.request_id, message: 'Test queued for your confirmed email. Check delivery history for the result.' } })
|
||||
}
|
||||
if (request.method() === 'PUT') { settings = { ...payload, next_send_at: payload.enabled ? Date.UTC(2026, 9, payload.day, payload.hour) / 1000 : null }; return route.fulfill({ json: settings }) }
|
||||
return route.fulfill({ json: { settings, months, ready: true, detail: 'Ready', deliveries, total: deliveries.length, subscribers: 12, worker_enabled: true } })
|
||||
}
|
||||
if (url.pathname.startsWith('/api/email-recaps/')) {
|
||||
if (url.pathname.endsWith('/confirm')) tokenState = payload.action === 'confirm' ? 'enabled' : 'off'
|
||||
return route.fulfill({ json: { action: payload.action, state: tokenState } })
|
||||
}
|
||||
if (url.pathname.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' })
|
||||
return route.fulfill({ json: {} })
|
||||
})
|
||||
const page = await context.newPage()
|
||||
const errors = []
|
||||
page.on('pageerror', (error) => errors.push(error.message))
|
||||
if (output) fs.mkdirSync(output, { recursive: true })
|
||||
for (const width of [1440, 980, 390, 320]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
await page.goto(`${base}/admin/recaps`)
|
||||
await page.getByRole('heading', { name: 'Monthly schedule', exact: true }).waitFor()
|
||||
assert.equal(await page.getByLabel('Enable scheduled monthly recaps').isChecked(), false)
|
||||
assert.equal(await page.getByRole('button', { name: 'Send test to me', exact: true }).isDisabled(), true)
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Admin overflow at ${width}`)
|
||||
if (output) await page.screenshot({ path: path.join(output, `recaps-admin-${width}.png`), fullPage: true })
|
||||
await page.getByRole('button', { name: 'Preview my recap', exact: true }).click()
|
||||
const frame = page.frameLocator('iframe[title="Monthly recap email preview"]')
|
||||
await frame.getByRole('heading', { name: 'August 2026', exact: true }).waitFor()
|
||||
assert.equal(await page.locator('iframe').getAttribute('sandbox'), '')
|
||||
assert.equal(await frame.getByRole('link', { name: /Explore your full report/ }).getAttribute('href'), 'https://beta.example.test/insights/reports?month=2026-08')
|
||||
assert(await frame.locator('body').evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Email overflow at ${width}`)
|
||||
if (output) await page.locator('.recap-preview').screenshot({ path: path.join(output, `recap-preview-${width}.png`) })
|
||||
await page.getByRole('button', { name: 'Plain text', exact: true }).click()
|
||||
assert.match(await page.locator('.recap-plain-preview').innerText(), /Minutes watched: 1,500/)
|
||||
await page.goto(`${base}/profile`)
|
||||
await page.getByRole('heading', { name: 'Your month, delivered.', exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: 'Email me my monthly recap', exact: true }).waitFor()
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Profile overflow at ${width}`)
|
||||
if (output) await page.screenshot({ path: path.join(output, `recap-profile-${width}.png`), fullPage: true })
|
||||
}
|
||||
await page.getByRole('button', { name: 'Email me my monthly recap', exact: true }).click()
|
||||
await page.getByText('Check your inbox', { exact: true }).waitFor()
|
||||
assert.equal(await page.getByRole('button', { name: 'Send a new confirmation', exact: true }).isDisabled(), true)
|
||||
await page.getByRole('button', { name: 'Cancel subscription', exact: true }).click()
|
||||
await page.getByText('Off', { exact: true }).waitFor()
|
||||
preference = { ...preference, state: 'enabled' }
|
||||
await page.getByRole('button', { name: 'Refresh preference', exact: true }).click()
|
||||
await page.getByText('Subscribed', { exact: true }).waitFor()
|
||||
await page.getByRole('textbox', { name: 'Email address', exact: true }).fill('changed@example.test')
|
||||
await page.getByRole('button', { name: 'Save email', exact: true }).click()
|
||||
await page.getByText('Off', { exact: true }).waitFor()
|
||||
await page.goto(`${base}/admin/recaps`)
|
||||
await page.getByLabel('Day of the month').selectOption('3')
|
||||
await page.getByLabel('Enable scheduled monthly recaps').check()
|
||||
assert.equal(await page.getByRole('button', { name: 'Preview my recap', exact: true }).isDisabled(), true)
|
||||
await page.getByRole('button', { name: 'Save schedule', exact: true }).click()
|
||||
await page.getByText('Schedule running', { exact: true }).waitFor()
|
||||
assert.equal(settings.day, 3)
|
||||
assert.equal(settings.enabled, true)
|
||||
await page.getByRole('button', { name: 'Preview my recap', exact: true }).click()
|
||||
await page.locator('iframe').waitFor()
|
||||
await page.getByRole('button', { name: 'Send test to me', exact: true }).click()
|
||||
await page.getByText('Queued', { exact: true }).waitFor()
|
||||
deliveries = ['sent', 'retry', 'unknown', 'failed', 'cancelled'].map((state, index) => ({ ...deliveries[0], id: `fixture-${index}`, state, attempts: index + 1, detail: state === 'unknown' ? 'Check the mail server.' : 'Fixture delivery result.' }))
|
||||
await page.getByRole('button', { name: 'Refresh history', exact: true }).click()
|
||||
await page.getByText('Needs review', { exact: true }).waitFor()
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'History overflows mobile viewport')
|
||||
assert.match(await page.locator('.recap-history').innerText(), /Automatic retries are stopped/)
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
if (output) await page.screenshot({ path: path.join(output, 'recap-delivery-history.png'), fullPage: true })
|
||||
failPreview = true
|
||||
await page.getByRole('button', { name: 'Preview my recap', exact: true }).click()
|
||||
await page.getByRole('alert').filter({ hasText: 'temporarily unavailable' }).waitFor()
|
||||
await context.clearCookies()
|
||||
const token = 'fixture'.repeat(7)
|
||||
tokenState = 'ready'
|
||||
await page.goto(`${base}/email-recaps#action=confirm&token=${token}`)
|
||||
await page.getByRole('button', { name: 'Confirm email recaps', exact: true }).waitFor()
|
||||
assert.equal(calls.filter((call) => call.path === '/api/email-recaps/confirm').length, 0)
|
||||
assert.equal(await page.locator('.header').count(), 0)
|
||||
await page.getByRole('button', { name: 'Confirm email recaps', exact: true }).click()
|
||||
await page.getByRole('heading', { name: 'You’re on the list.', exact: true }).waitFor()
|
||||
assert.equal(new URL(page.url()).hash, '')
|
||||
tokenState = 'ready'
|
||||
await page.setViewportSize({ width: 320, height: 800 })
|
||||
await page.goto(`${base}/email-recaps#action=unsubscribe&token=${token}`)
|
||||
await page.getByRole('button', { name: 'Unsubscribe from recaps', exact: true }).waitFor()
|
||||
if (output) await page.screenshot({ path: path.join(output, 'recap-unsubscribe-mobile.png'), fullPage: true })
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Unsubscribe overflow')
|
||||
await page.getByRole('button', { name: 'Unsubscribe from recaps', exact: true }).click()
|
||||
await page.getByRole('heading', { name: 'Recaps are turned off.', exact: true }).waitFor()
|
||||
await page.goto(`${base}/email-recaps`)
|
||||
await page.getByRole('alert').filter({ hasText: 'incomplete' }).waitFor()
|
||||
role = 'user'
|
||||
await page.goto(`${base}/admin/recaps`)
|
||||
await page.waitForURL(`${base}/`)
|
||||
role = 'unauthorized'
|
||||
await page.goto(`${base}/admin/recaps`)
|
||||
await page.waitForURL(/\/login\?next=/)
|
||||
assert.deepEqual(errors, [])
|
||||
console.log(`Email recap UI passed: desktop/mobile layout, preview isolation, consent, schedule, test queue, history, public links and access control; ${calls.length} intercepted API calls.`)
|
||||
} finally { await browser.close() }
|
||||
})().catch((error) => { console.error(error); process.exitCode = 1 })
|
||||
@@ -92,6 +92,15 @@ const output = process.env.REVIEW_DIR
|
||||
await page.screenshot({ path: path.join(output, `monthly-report-${width}.png`), fullPage: true })
|
||||
}
|
||||
}
|
||||
const beforeLink = calls.length
|
||||
await page.goto(base + '/insights/reports?month=2026-07')
|
||||
await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor()
|
||||
const linkedCalls = calls.slice(beforeLink).filter((call) => call.path === '/api/insights/reports/monthly')
|
||||
assert.deepEqual(linkedCalls.map((call) => call.month), ['2026-07'])
|
||||
await page.reload()
|
||||
await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor()
|
||||
await page.goto(base + '/insights/reports')
|
||||
await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: 'Previous month', exact: true }).click()
|
||||
await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: 'Next month', exact: true }).click()
|
||||
@@ -126,7 +135,8 @@ const output = process.env.REVIEW_DIR
|
||||
assert.equal(await page.getByRole('link', { name: 'Connect Jellystat' }).count(), 0)
|
||||
mode = 'unauthorized'
|
||||
await page.reload()
|
||||
await page.waitForURL('**/login?next=%2Finsights%2Freports')
|
||||
await page.waitForURL(/\/login\?next=/)
|
||||
assert.equal(new URL(page.url()).searchParams.get('next'), `/insights/reports?month=${months.at(-1)}`)
|
||||
mode = 'ready'
|
||||
await page.goto(base + '/insights/reports')
|
||||
await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor()
|
||||
|
||||
Reference in New Issue
Block a user