Files
houseplan-card/test/support-feedback.test.mjs
T
2026-09-02 00:05:36 +00:00

87 lines
4.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
codePointLength,
newSupportDialogState,
supportCanSubmit,
supportDraftError,
supportErrorCode,
supportRuntimeFacts,
} from '../test-build/support-feedback.js';
test('a fresh dialog never opts into exact plan geometry', () => {
const state = newSupportDialogState();
assert.equal(state.attach, false);
assert.equal(state.preview, null);
assert.equal(state.contact, '');
assert.equal(state.message, '');
assert.match(state.draftId, /^draft-/);
assert.match(state.idempotencyKey, /^report-/);
});
test('message/contact limits count Unicode code points rather than UTF-16 units', () => {
assert.equal(codePointLength('🙂'), 1);
const state = { ...newSupportDialogState(), message: 'ok', contact: '🙂'.repeat(321) };
assert.equal(supportDraftError(state), 'contact_too_long');
assert.equal(supportCanSubmit(state), false);
});
test('attachment submit requires the exact non-expired preview', () => {
const now = 10_000;
const base = { ...newSupportDialogState(), message: 'repro', attach: true };
assert.equal(supportDraftError(base, now), 'preview_missing');
const preview = {
token: 'a'.repeat(48), expiresAt: now + 1, size: 2, sha256: 'b'.repeat(64),
spaces: 1, format: 'houseplan-support-package', version: 1, text: '{}\n', preparedAt: now,
};
assert.equal(supportCanSubmit({ ...base, preview, status: 'ready' }, now), true);
assert.equal(supportDraftError({ ...base, preview }, now + 1), 'preview_expired');
});
test('runtime facts are bounded enums without a raw user agent', () => {
const facts = supportRuntimeFacts({
userAgent: 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 private-suffix',
language: 'ru', coarsePointer: false, hoverCapable: true,
registryAccess: 'full', registryLastSuccess: 9_500, now: 10_000,
});
assert.deepEqual(facts, {
browser_family: 'chromium', browser_major: 140, language: 'ru',
coarse_pointer: false, hover_capable: true, registry_access: 'full',
registry_age_bucket: 'fresh',
});
assert.equal(JSON.stringify(facts).includes('private-suffix'), false);
});
test('remote and unknown failures collapse to stable local error codes', () => {
assert.equal(supportErrorCode({ code: 'support_rate_limited', message: 'private' }), 'support_rate_limited');
assert.equal(supportErrorCode({ code: 'provider leaked details' }), 'support_unavailable');
assert.equal(supportErrorCode(new Error('network secret')), 'support_unavailable');
});
test('Help is lazy, ordered after settings, and owns the single About/Guide surface', () => {
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const runtime = readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
const styles = readFileSync(new URL('../src/styles/plan.styles.ts', import.meta.url), 'utf8');
const header = card.slice(card.indexOf('<div class="zoomctl">'), card.indexOf('</div>\n ${this._canEdit'));
assert.ok(header.indexOf('_openSettingsDialog') < header.indexOf('_openSupportDialog'));
assert.match(card, /if \(!this\._editorRuntime\)[\s\S]*?_ensureEditorRuntime\(\)[\s\S]*?_openSupportDialog/);
assert.equal((runtime.match(/_t\('gs\.about_version'/g) || []).length, 1);
assert.match(runtime, /docs\/USER-GUIDE\.ru\.md/);
assert.match(runtime, /docs\/USER-GUIDE\.md/);
assert.match(styles, /\.support-button\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/);
});
test('the consent copy names exact geometry, project relay, retention and network address', () => {
const en = JSON.parse(readFileSync(new URL('../src/i18n/en.json', import.meta.url), 'utf8'));
const ru = JSON.parse(readFileSync(new URL('../src/i18n/ru.json', import.meta.url), 'utf8'));
assert.match(en['support.privacy'], /exact geometry/);
assert.match(en['support.privacy'], /project relay/);
assert.match(en['support.privacy'], /30 days/);
assert.match(en['support.privacy'], /server address/);
assert.equal(
ru['support.contact'],
'Контакт для связи (email/tg/WhatsApp), необязательно.',
);
});