191 lines
12 KiB
JavaScript
191 lines
12 KiB
JavaScript
// Install screen + Store screenshots and flow checks on the preview harnesses (needs `vite dev`;
|
|
// native install/store commands are mocked in src/shell/preview/storeMocks.ts with the real store
|
|
// files from apps/web/public/store). Not a native test: signature, allow-list, size caps and
|
|
// validation are Rust's (distribution.rs, waybetter-host plugins.rs) and tested there.
|
|
// Usage: node scripts/store-shots.mjs [baseUrl] [outDir]
|
|
import { chromium } from 'playwright';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const base = process.argv[2] ?? 'http://127.0.0.1:1420';
|
|
const out = process.argv[3] ?? join(import.meta.dirname, '..', '..', '..', 'verification', 'store');
|
|
mkdirSync(out, { recursive: true });
|
|
|
|
const failures = [];
|
|
const check = (label, ok, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`); if (!ok) failures.push(label); };
|
|
const browser = await chromium.launch();
|
|
|
|
async function open(path, viewport = { width: 1280, height: 860 }) {
|
|
const page = await browser.newPage({ viewport, deviceScaleFactor: 1 });
|
|
page.on('pageerror', error => failures.push(`page error: ${error.message}`));
|
|
const url = new URL(path, `${base}/`);
|
|
if (!url.searchParams.has('lang')) url.searchParams.set('lang', 'en');
|
|
await page.goto(url.href);
|
|
return page;
|
|
}
|
|
const card = (page, name) => page.locator('.wbs-card').filter({ has: page.locator('h3', { hasText: new RegExp(`^${name}$`) }) });
|
|
const shot = (page, name) => page.screenshot({ path: join(out, `${name}.png`) });
|
|
|
|
// 1. A waybetter://install link for a plugin arrives in the shell: the screen shows everything, and
|
|
// Install stays disabled for a moment.
|
|
{
|
|
const page = await open('?shell&tabs=4&open=install&kind=plugin');
|
|
await page.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
const install = page.locator('.wbi-install');
|
|
check('link: Install is disabled at first', await install.isDisabled());
|
|
const text = await page.locator('.wbi').innerText();
|
|
check('link: name, author, version', text.includes('Reply Guard') && text.includes('by wayBetter') && /v\d+\.\d+\.\d+/.test(text));
|
|
check('link: source host shown', text.includes('Link: waybetter.fyi'));
|
|
check('link: sha256 shown', /[0-9a-f]{64}/.test(text));
|
|
check('link: permissions listed', ['Storage', 'Dialogs', 'Your account', 'Account details', 'No network'].every(label => text.includes(label)));
|
|
check('link: honest note about page access', text.includes('Permissions limit what the wayBetter API gives a plugin, not the page'));
|
|
check('link: code preview', (await page.locator('.wbi-code .wbi-line').count()) > 20);
|
|
// Focus starts on Cancel: Enter cancels, it never installs.
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForTimeout(200);
|
|
check('link: Enter does not install', (await page.locator('.wbi.is-done').count()) === 0 && (await page.locator('.wbi-install').count()) === 0);
|
|
await page.evaluate(() => window.__btEmit('install-link', { link: { kind: 'plugin', url: 'https://waybetter.fyi/store/plugins/bt-reply-guard.plugin.js' } }));
|
|
await page.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
await page.waitForTimeout(1100);
|
|
check('link: Install enabled after review time', !(await install.isDisabled()));
|
|
await shot(page, 'install-plugin-link');
|
|
await page.locator('.wbi-body').evaluate(node => { node.scrollTop = node.scrollHeight; });
|
|
await shot(page, 'install-plugin-link-code');
|
|
await install.click();
|
|
await page.waitForSelector('.wbi.is-done', { timeout: 5000 });
|
|
check('link: installed state', (await page.locator('.wbi').innerText()).includes('Reply Guard is installed'));
|
|
await shot(page, 'install-plugin-done');
|
|
await page.close();
|
|
}
|
|
|
|
// 2. A theme link: live preview of the theme, file preview, "Use it now".
|
|
{
|
|
const page = await open('?shell&tabs=3&open=install&kind=theme&src=https://waybetter.fyi/store/themes/paper.json');
|
|
await page.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
const text = await page.locator('.wbi').innerText();
|
|
check('theme: name and preview', text.includes('Paper') && (await page.locator('.wbi-theme-preview').count()) === 1);
|
|
check('theme: no permissions block', (await page.locator('.wbi-perms').count()) === 0);
|
|
await page.waitForTimeout(1100);
|
|
await shot(page, 'install-theme-link');
|
|
await page.keyboard.press('Escape');
|
|
check('theme: Esc cancels', (await page.locator('.wbi').count()) === 0);
|
|
await page.close();
|
|
}
|
|
|
|
// 3. A refused link: an explanation, nothing downloaded.
|
|
{
|
|
const page = await open('?shell&tabs=3&open=install-error');
|
|
await page.waitForSelector('.wbi.is-error', { timeout: 8000 });
|
|
check('refused link explained', (await page.locator('.wbi').innerText()).includes('only https links on waybetter.fyi'));
|
|
await shot(page, 'install-link-refused');
|
|
await page.close();
|
|
}
|
|
|
|
// 4. Dropping files on the window: a plugin file (legacy, no @permissions; its code calls fetch),
|
|
// then a file that is neither.
|
|
{
|
|
const page = await open('?shell&tabs=3');
|
|
await page.waitForSelector('.bt-tab');
|
|
const drop = async (name, content, type = 'text/javascript') => {
|
|
const data = await page.evaluateHandle(([n, c, t]) => { const dt = new DataTransfer(); dt.items.add(new File([c], n, { type: t })); return dt; }, [name, content, type]);
|
|
await page.dispatchEvent('.bt-titlebar', 'dragenter', { dataTransfer: data });
|
|
await page.dispatchEvent('.bt-titlebar', 'dragover', { dataTransfer: data });
|
|
await page.waitForTimeout(150);
|
|
return data;
|
|
};
|
|
const plugin = '/**\n * @name Weather Badge\n * @description Shows the weather next to the search box.\n * @version 0.3.0\n * @author someone\n */\nmodule.exports = class {\n async start() {\n const r = await fetch("https://api.example/weather");\n this.el = document.body.appendChild(document.createElement("div"));\n this.el.textContent = (await r.json()).summary;\n }\n stop() { this.el?.remove(); }\n};\n';
|
|
const data = await drop('weather-badge.plugin.js', plugin);
|
|
check('drop: window-wide drop target', (await page.locator('.wbi-drop').count()) === 1);
|
|
await shot(page, 'drop-target');
|
|
await page.dispatchEvent('.bt-titlebar', 'drop', { dataTransfer: data });
|
|
await page.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
const text = await page.locator('.wbi').innerText();
|
|
check('drop: dropped-file source', text.includes('A file you dropped'));
|
|
check('drop: legacy badge', text.includes('Legacy · full page access'));
|
|
check('drop: source scan finding', text.includes('makes network requests (fetch)'));
|
|
await page.waitForTimeout(1100);
|
|
await shot(page, 'install-dropped-legacy-plugin');
|
|
await page.locator('.wbi-foot .bt-deck-secondary').click();
|
|
const other = await drop('photo.png', 'PNG', 'image/png');
|
|
await page.dispatchEvent('.bt-titlebar', 'drop', { dataTransfer: other });
|
|
await page.waitForSelector('.wbi.is-error', { timeout: 5000 });
|
|
check('drop: other files are explained, not installed', (await page.locator('.wbi').innerText()).includes('drop it on X’s composer'));
|
|
await page.close();
|
|
|
|
const turkish = await open('?shell&tabs=3&lang=tr');
|
|
await turkish.waitForSelector('.bt-tab');
|
|
const turkishFile = await turkish.evaluateHandle(([name, content]) => {
|
|
const transfer = new DataTransfer();
|
|
transfer.items.add(new File([content], name, { type: 'text/javascript' }));
|
|
return transfer;
|
|
}, ['weather-badge.plugin.js', plugin]);
|
|
await turkish.dispatchEvent('.bt-titlebar', 'dragenter', { dataTransfer: turkishFile });
|
|
await turkish.dispatchEvent('.bt-titlebar', 'dragover', { dataTransfer: turkishFile });
|
|
await turkish.dispatchEvent('.bt-titlebar', 'drop', { dataTransfer: turkishFile });
|
|
await turkish.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
check('drop: Turkish source label', (await turkish.locator('.wbi-source strong').innerText()) === 'Bıraktığın bir dosya');
|
|
await turkish.close();
|
|
}
|
|
|
|
// 5. The Store section: cards, search, categories, installed / update states, permission badges;
|
|
// installing from it goes through the same screen.
|
|
{
|
|
const page = await open('?panel§ion=store', { width: 1180, height: 900 });
|
|
await page.waitForSelector('.wbs-card:not(.is-skeleton)', { timeout: 8000 });
|
|
await page.waitForTimeout(600);
|
|
const cards = await page.locator('.wbs-card').count();
|
|
check('store: cards for every catalog item', cards >= 27, `${cards}`);
|
|
check('store: signature status', (await page.locator('.wbs-status.is-ok').innerText()).includes('Signed catalog verified'));
|
|
check('store: installed plugin', (await card(page, 'Thread Reader').innerText()).includes('Installed'));
|
|
check('store: update available', (await card(page, 'Hide Blue Checks').innerText()).includes('Update to'));
|
|
check('store: built-in theme', (await card(page, 'Nord').innerText()).includes('Built in'));
|
|
check('store: updates banner', (await page.locator('.wbs-updates').count()) === 1);
|
|
await shot(page, 'store');
|
|
await page.locator('.wbs-toolbar input').fill('regex');
|
|
check('store: search', (await page.locator('.wbs-card').count()) === 1);
|
|
await page.locator('.wbs-toolbar input').fill('');
|
|
await page.locator('.wbs-toolbar [role="radiogroup"] button, .wbs-toolbar button', { hasText: 'Plugins' }).first().click();
|
|
await page.locator('.wbs-cats button', { hasText: 'Reading' }).click();
|
|
await page.waitForTimeout(200);
|
|
await shot(page, 'store-plugins-reading');
|
|
check('store: category filter', (await page.locator('.wbs-card').count()) === 3);
|
|
await page.locator('.wbs-cats button', { hasText: 'Everything' }).click();
|
|
await card(page, 'Copy as Markdown').locator('button', { hasText: 'Install' }).click();
|
|
await page.waitForSelector('.wbi.is-ready', { timeout: 8000 });
|
|
const text = await page.locator('.wbi').innerText();
|
|
check('store install: signed source', text.includes('wayBetter Store') && text.includes('Verified'));
|
|
await page.waitForTimeout(1100);
|
|
await shot(page, 'install-from-store');
|
|
await page.locator('.wbi-install').click();
|
|
await page.waitForSelector('.wbi.is-done');
|
|
await page.locator('.wbi-state-actions .bt-deck-primary').click();
|
|
await page.waitForTimeout(400);
|
|
check('store: installed after confirming', (await card(page, 'Copy as Markdown').innerText()).includes('Installed'));
|
|
// The Plugins list shows permissions, the legacy badge and where a plugin came from.
|
|
await page.locator('.btp-nav button', { hasText: 'Plugins' }).click();
|
|
await page.waitForTimeout(300);
|
|
const list = await page.locator('.btp-page').innerText();
|
|
check('plugins list: permission badges', list.includes('Clipboard') && list.includes('No network'));
|
|
check('plugins list: legacy badge', list.includes('Legacy · full page access'));
|
|
check('plugins list: store source', list.includes('Store'));
|
|
await shot(page, 'plugins-permissions');
|
|
await page.close();
|
|
}
|
|
|
|
// 6. Narrow panel and an unreachable store.
|
|
{
|
|
const page = await open('?panel§ion=store', { width: 460, height: 900 });
|
|
await page.waitForSelector('.wbs-card:not(.is-skeleton)', { timeout: 8000 });
|
|
await page.waitForTimeout(500);
|
|
await shot(page, 'store-narrow');
|
|
await page.close();
|
|
const offline = await open('?panel§ion=store&offline', { width: 1180, height: 700 });
|
|
await offline.waitForSelector('.bt-empty', { timeout: 8000 });
|
|
check('store offline: explained', (await offline.locator('.bt-empty').innerText()).includes('not reachable'));
|
|
await shot(offline, 'store-offline');
|
|
await offline.close();
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(failures.length ? `\n${failures.length} failure(s):\n- ${failures.join('\n- ')}` : '\nall checks passed');
|
|
process.exit(failures.length ? 1 : 0);
|