waybetter/packages/shell/tests/report.test.ts
CRT 8e9442c2fc Report a problem: the webview's full version (the user agent carries a reduced one)
Seen live: the report said "WebView2 154.0.0.0", Chromium's reduced user agent version. The engine's brand list (`fullVersionList`) gives the full one, which the report now uses when it is a plain version (tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:56:18 +07:00

118 lines
7.3 KiB
TypeScript

// "Report a problem" (frame/reportModel.ts): the summary says versions and what is switched on, and
// cannot say anything about the person: it has no field for an account, an address or a path, and
// every value is reduced to id characters before it is written.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { FRAMEWORK_VERSION, buildReport, reportFileName, reportValue, systemFromUserAgent, webviewFromUserAgent, type ReportInfo } from '../src/frame/reportModel.ts';
const here = dirname(fileURLToPath(import.meta.url));
const INFO: ReportInfo = {
product: 'wayBetter for X', appVersion: '1.3.2', system: 'Windows 11 (x64)', webview: 'WebView2 141.0.3537.71',
language: 'tr', languageChoice: 'site', mode: 'advanced', theme: 'night-ink', iconPack: 'phosphor',
plugins: ['reply-guard.plugin.js', 'mute-words.plugin.js'],
details: [{ label: 'Paused features', values: ['composeCounter'] }],
safeMode: false,
};
test('the summary is one fact per line, in English', () => {
assert.equal(buildReport(INFO), [
'wayBetter for X 1.3.2',
`Framework: wayBetter ${FRAMEWORK_VERSION}`,
'System: Windows 11 (x64)',
'Webview: WebView2 141.0.3537.71',
'Language: tr (chosen: site)',
'Mode: advanced',
'Theme: night-ink',
'Icon pack: phosphor',
'Plugins on: reply-guard.plugin.js, mute-words.plugin.js',
'Paused features: composeCounter',
'Safe mode: no',
].join('\n'));
});
test('an app without modes, themes or plugins says less, never something made up', () => {
const text = buildReport({ product: 'App', appVersion: '', system: '', webview: '', language: '', languageChoice: '', plugins: null, safeMode: true });
assert.equal(text, ['App unknown version', `Framework: wayBetter ${FRAMEWORK_VERSION}`, 'System: unknown', 'Webview: unknown', 'Language: unknown (chosen: unknown)', 'Plugins on: unknown', 'Safe mode: yes'].join('\n'));
assert.match(buildReport({ ...INFO, mode: null, plugins: [], iconPack: '', details: [{ label: 'Paused features', values: [] }] }), /Mode: none chosen\n[\s\S]*Icon pack: the site's own\nPlugins on: none\nPaused features: none\n/);
});
test('nothing about the person can ride along in a value', () => {
const hostile: ReportInfo = {
product: 'wayBetter for X <script>', appVersion: '1.3.2 @someone https://x.com/someone',
system: 'Windows 11 (x64) C:\\Users\\someone', webview: 'WebView2 141 https://example.com/?q=secret',
language: 'tr@someone', languageChoice: 'https://x.com/home',
mode: 'advanced\nAccount: @someone', theme: '../../Users/someone/night-ink', iconPack: 'file:///C:/Users/someone/pack',
plugins: ['C:\\Users\\someone\\AppData\\plugins\\reply-guard.plugin.js', '/home/someone/.config/app/plugins/a b.plugin.js', '@someone', 'https://x.com/someone/status/1'],
details: [{ label: 'Open tabs: https://x.com/messages', values: ['https://x.com/messages/123', '@someone', 'ok-id'] }],
safeMode: false,
};
const text = buildReport(hostile);
// Nothing of it got through: no name, no address, no path, not even squeezed together.
for (const forbidden of ['someone', 'secret', '@', '\\', 'http', 'x.com', 'Users', 'AppData', 'home', 'messages', '<', '?']) {
assert.ok(!text.includes(forbidden), `${JSON.stringify(forbidden)} in\n${text}`);
}
assert.equal(text, [
'App unknown version',
`Framework: wayBetter ${FRAMEWORK_VERSION}`,
'System: unknown',
'Webview: unknown',
'Language: unknown (chosen: unknown)',
'Mode: unknown',
'Theme: unknown',
'Icon pack: unknown',
// A file name stands for itself, never for where it is; what is not a plain name is only counted.
'Plugins on: reply-guard.plugin.js, 1, and 2 more',
'Safe mode: no',
].join('\n'));
assert.equal(reportFileName('C:\\Users\\someone\\x.plugin.js'), 'x.plugin.js');
assert.equal(reportFileName('/opt/app/plugins/a b.plugin.js'), '', 'not trimmed into shape');
assert.equal(reportValue('a'.repeat(200)), '');
assert.equal(reportValue('night-ink'), 'night-ink');
assert.equal(reportValue(12), '12');
assert.equal(reportValue({} as never), '');
// Long lists are cut, and say so.
const many = buildReport({ ...INFO, plugins: Array.from({ length: 75 }, (_, n) => `p${n}.plugin.js`) });
assert.match(many, /p59\.plugin\.js, and 15 more$/m);
assert.match(buildReport({ ...INFO, plugins: ['@someone'] }), /^Plugins on: 1 not shown$/m);
});
test('the model has no field for an account, an address, a tab or a path', () => {
const source = readFileSync(join(here, '../src/frame/reportModel.ts'), 'utf8');
const type = source.slice(source.indexOf('export type ReportInfo'), source.indexOf('const MAX_ITEMS'));
for (const forbidden of ['account', 'handle', 'url', 'tab', 'path', 'profile', 'user', 'email', 'title']) {
assert.ok(!new RegExp(`\\b${forbidden}\\w*\\??:`, 'i').test(type), `a ${forbidden} field`);
}
// The dialog never sends the summary: no fetch, and the issue page opens bare.
const dialog = readFileSync(join(here, '../src/frame/ReportDialog.tsx'), 'utf8');
assert.ok(!/\bfetch\(|XMLHttpRequest|sendBeacon|encodeURIComponent|searchParams|\?body=|\$\{text\}/.test(dialog), 'nothing is sent and nothing is put in a URL');
assert.match(dialog, /invoke\('open_external_url', \{ url: issueUrl \}\)/);
});
test('the framework version is the package version', () => {
const pkg = JSON.parse(readFileSync(join(here, '../../waybetter/package.json'), 'utf8')) as { version: string };
assert.equal(FRAMEWORK_VERSION, pkg.version);
});
test('the system and the webview are read coarsely from the user agent', () => {
const edge = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.3537.71';
assert.equal(webviewFromUserAgent(edge), 'WebView2 141.0.3537.71');
// A reduced user agent (seen live: "154.0.0.0") and the engine's brand list with the full version.
const reduced = edge.replace('141.0.3537.71', '154.0.0.0');
assert.equal(webviewFromUserAgent(reduced), 'WebView2 154.0.0.0');
assert.equal(webviewFromUserAgent(reduced, [{ brand: 'Chromium', version: '154.0.7000.1' }, { brand: 'Microsoft Edge WebView2', version: '154.0.3700.12' }]), 'WebView2 154.0.3700.12');
assert.equal(webviewFromUserAgent(reduced, [{ brand: 'Microsoft Edge WebView2', version: '154; drop' }]), 'WebView2 154.0.0.0');
assert.equal(systemFromUserAgent(edge), 'Windows 10 or 11 (x64)');
assert.equal(systemFromUserAgent(edge, { platformVersion: '15.0.0', architecture: 'x86', bitness: '64' }), 'Windows 11 (x86_64)');
assert.equal(systemFromUserAgent(edge, { platformVersion: '10.0.0' }), 'Windows 10 (x64)');
const mac = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)';
assert.equal(webviewFromUserAgent(mac), 'WebKit 605.1.15');
assert.equal(systemFromUserAgent(mac), 'macOS');
const linux = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15';
assert.equal(systemFromUserAgent(linux), 'Linux (x86_64)');
assert.equal(webviewFromUserAgent(linux), 'WebKit 605.1.15');
assert.equal(systemFromUserAgent('something else'), 'unknown');
assert.equal(webviewFromUserAgent('something else'), 'unknown');
});