mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
feat: add private help and feedback reports (#43)
Issue: #43 User-Visible: yes
This commit is contained in:
@@ -64,10 +64,10 @@ test('all dangerous-action call sites use the shared confirmation contract', ()
|
||||
const sharedCalls = source.match(/await this(?:\.host)?\._confirmDanger\s*\(\{/g) || [];
|
||||
|
||||
assert.equal(nativeCalls.length, 0, 'native browser confirmation must not return');
|
||||
assert.equal(sharedCalls.length, 8, 'the reviewed inventory stays on the shared surface');
|
||||
assert.equal(sharedCalls.length, 9, 'the reviewed inventory stays on the shared surface');
|
||||
for (const key of [
|
||||
'delete-draft', 'delete-draft-segment', 'remove-marker',
|
||||
'delete-plan', 'delete-space', 'unlock',
|
||||
'delete-plan', 'delete-space', 'unlock', 'close-support-busy',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`key: '${key}'`));
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ test('paths classify into A/B/C/D with the generated tree winning over source',
|
||||
assert.equal(classify('demo/golden/baselines/view.png'), 'D');
|
||||
assert.equal(classify('dist/houseplan-card.js'), 'D');
|
||||
assert.equal(classify('test/canvas.test.mjs'), 'B');
|
||||
assert.equal(classify('scripts/support-relay/relay.py'), 'B');
|
||||
assert.equal(classify('.github/workflows/validate.yml'), 'B');
|
||||
assert.equal(classify('package-lock.json'), 'B');
|
||||
assert.equal(classify('docs/SCOPE.md'), 'C');
|
||||
@@ -712,6 +713,16 @@ test('an infrastructure range is recognised by the absence of class A files (#20
|
||||
assert.equal(isInfrastructureRange([]), false);
|
||||
});
|
||||
|
||||
test('a support-relay-only change stays on the reviewed class-B track (#43)', () => {
|
||||
const relay = makeCommit({
|
||||
sha: 'd'.repeat(40), subject: 'Harden private support relay',
|
||||
body: 'Issue: #43\nUser-Visible: no',
|
||||
files: ['scripts/support-relay/hp_relay/app.py', 'scripts/support-relay/tests/test_relay.py'],
|
||||
});
|
||||
assert.deepEqual([...relay.classes], ['B']);
|
||||
assert.equal(isInfrastructureRange([relay]), true);
|
||||
});
|
||||
|
||||
test('statusOptional waives the status label but keeps every other rule-8 refusal (#207)', () => {
|
||||
// Метки инфраструктурного issue по #118: тип, приоритет, тема — без S*.
|
||||
const infraIssue = () => ({
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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), необязательно.',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user