chore: migrate mail themes to shared framework and locked upstream inputs
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s

This commit is contained in:
KenanZhu committed 2026-10-09 18:34:36 +08:00
1 parent 5c0589f6f6
commit fec3ace600
225 files changed
+4718 -15807

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
{
"name": "gitea-mail-preview-qa",
"private": true,
"scripts": { "test": "node preview.cjs" },
"devDependencies": { "playwright": "1.62.1", "sharp": "0.35.5" }
}
+98
View File
@@ -0,0 +1,98 @@
// Optional browser QA: npm install in this directory, then npm test.
const { chromium } = require('playwright');
const sharp = require('sharp');
const fs = require('node:fs/promises');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const root = path.resolve(__dirname, '../..');
async function ready(page) {
await page.waitForFunction(() => document.getElementById('preview-frame').style.display === 'block' && document.getElementById('loading').style.display === 'none');
const frame = page.frameLocator('#preview-frame');
await frame.locator('body').waitFor();
return frame;
}
(async () => {
const browser = await chromium.launch({headless:true, executablePath:process.env.BROWSER_EXECUTABLE_PATH || undefined});
try {
const page = await browser.newPage({viewport:{width:1650,height:1050}});
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.goto(pathToFileURL(path.join(root,'preview/index.html')).href);
await page.addStyleTag({content:'*{transition:none!important;animation:none!important}'});
await ready(page);
// Floating inspector is preview chrome, not part of the rendered email.
if (await page.locator('#panel-close').isVisible()) await page.click('#panel-close');
const languages = await page.locator('#sel-locale option').evaluateAll(options => options.map(o=>o.value));
const themes = await page.locator('#sel-theme option').evaluateAll(options => options.map(o=>o.value));
const types = await page.locator('#sel-template option').evaluateAll(options => options.map(o=>o.value));
const buttonColors = {aurora:'rgb(224, 245, 243)',bloom:'rgb(255, 255, 255)',ember:'rgb(255, 255, 255)',heritage:'rgb(255, 255, 255)',horizon:'rgb(255, 255, 255)',ink:'rgb(254, 253, 251)',mono:'rgb(0, 0, 0)',neon:'rgb(10, 10, 15)',terminal:'rgb(13, 17, 23)',terra:'rgb(255, 255, 255)'};
if (!languages.includes('en-US') || !languages.includes('zh-CN')) throw new Error('official languages missing');
for (const lang of languages) {
await page.selectOption('#sel-locale',lang);
const frame = await ready(page);
if (!(await frame.locator('body').textContent()).trim()) throw new Error('empty '+lang+' preview');
}
await page.selectOption('#sel-locale','pl-PL');
await page.selectOption('#sel-template','team_invite');
await ready(page);
await page.waitForFunction(()=>document.getElementById('locale-message')?.textContent.includes('[UPSTREAM-WARN]'));
await page.selectOption('#sel-locale','zh-CN');
await page.selectOption('#sel-locale','en-US');
await ready(page);
const screenshotDir = path.join(root,'build','screenshots');
await fs.mkdir(screenshotDir,{recursive:true});
for (const theme of themes) {
await page.selectOption('#sel-theme',theme);
await page.selectOption('#sel-template','register_notify');
await page.click('#btn-desk');
const desktop = await ready(page);
const cta = desktop.locator('.card-body .email-btn').first();
const color = await cta.evaluate(el=>getComputedStyle(el).color);
if (buttonColors[theme] && color!==buttonColors[theme]) throw new Error('CTA styling overridden: '+theme+' '+color);
const label = (await cta.textContent()).trim();
if (!label || /^https?:/i.test(label)) throw new Error('button displays URL: '+theme);
const fallback = desktop.locator('.email-link-url a').first();
if (await cta.getAttribute('href') !== await fallback.getAttribute('href')) throw new Error('button/fallback target mismatch: '+theme);
const logo = desktop.locator('img.email-logo');
if (await logo.count()) {
if (!(await logo.getAttribute('src')).startsWith('data:image/png;base64,')) throw new Error('preview logo is not generated official asset: '+theme);
if (!(await logo.evaluate(img=>img.complete && img.naturalWidth>0))) throw new Error('logo failed to load: '+theme);
}
// Capture the actual 600px email, not the wide preview canvas/inspector.
await page.waitForFunction(()=>document.getAnimations().every(a=>a.playState!=='running'));
const box = await desktop.locator('.email-wrap').boundingBox();
if (!box) throw new Error('email frame missing: '+theme);
const png = await page.screenshot({clip:box,animations:'disabled'});
const background = await desktop.locator('body').evaluate(el=>getComputedStyle(el).backgroundColor);
const image = await sharp(png).extend({top:16,bottom:16,left:16,right:16,background}).resize({width:600}).png({compressionLevel:9}).toBuffer();
await fs.writeFile(path.join(screenshotDir,theme+'.png'),image);
for (const type of types) {
await page.selectOption('#sel-template',type);
await page.click('#btn-mob');
const frame = await ready(page);
const overflow = await frame.locator('html').evaluate(el=>el.scrollWidth > window.innerWidth + 1);
if (overflow) throw new Error('mobile overflow: '+theme+'/'+type);
}
}
if (errors.length) throw new Error(errors.join('\n'));
console.log('[PASS] file:// language bundles, warning, rapid switching, '+themes.length+' theme screenshots and mobile overflow');
// A running dev server can be included without making static QA depend on it.
if (process.env.PREVIEW_DEV_URL) {
await page.goto(process.env.PREVIEW_DEV_URL);
await ready(page);
await page.selectOption('#sel-locale','zh-CN');
await ready(page);
console.log('[PASS] HTTP preview and language switching');
}
if (process.argv.includes('--update-gallery')) {
for (const theme of themes) {
const image=await fs.readFile(path.join(screenshotDir,theme+'.png'));
if (image.length>50*1024) throw new Error('gallery image exceeds 50 KiB: '+theme);
await fs.writeFile(path.join(root,'docs','images',theme+'.png'),image);
}
console.log('[PASS] gallery screenshots updated');
}
} finally { await browser.close(); }
})().catch(error=>{console.error(error);process.exitCode=1;});