The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese, Korean and Hindi alongside English — 623 strings per language, 5,607 translations, covering the landing page, the key exchange, the chat, group calls and every error along the way. Each locale is a real page at a real URL (/de/, /fr/, …), generated at build time from locales/*.json. That is the whole point: the app is client-rendered, so a language that only swaps strings at runtime has no address for a crawler to index and no link anyone can share. Each page carries its own canonical, a reciprocal hreflang cluster and translated schema.org. The URL decides the language, always. A stored preference only applies where the address does not say, and nothing ever redirects on Accept-Language — that is how sites become invisible to search engines outside one country. robots.txt and sitemap.xml did not exist before; both are generated now. Bugs found on the way, each with a test that would have caught it: - partner logos used page-relative paths and 404'd from any /xx/ page - post-build stamped ?v= only into the root shell, leaving locales behind - "Back online" appeared on every page load, not just after being offline - update timestamps were hard-coded to US format for every reader - t() threw on a partial window, taking whole components down with it
94 lines
3.5 KiB
JavaScript
94 lines
3.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import vm from 'node:vm';
|
|
import { t } from '../src/i18n/index.js';
|
|
|
|
const effects = [];
|
|
const setterCalls = [];
|
|
let stateIndex = 0;
|
|
const callbackCalls = [];
|
|
|
|
const context = {
|
|
window: {},
|
|
// The component reaches its copy through t(); in the browser that arrives as an
|
|
// ES import, which a bare VM script cannot evaluate. Supply the real function so
|
|
// the component sees exactly what it sees at runtime.
|
|
t,
|
|
React: {
|
|
useState(initialValue) {
|
|
const index = stateIndex++;
|
|
return [initialValue, value => setterCalls.push({ index, value })];
|
|
},
|
|
useRef(initialValue) {
|
|
return { current: initialValue };
|
|
},
|
|
useEffect(effect) {
|
|
effects.push(effect);
|
|
},
|
|
createElement() {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
// vm.runInNewContext evaluates a script, not a module, so the import statements are
|
|
// stripped and their bindings handed in through the context above — the same
|
|
// substitution the bundler performs, done by hand.
|
|
const source = fs
|
|
.readFileSync(new URL('../src/components/ui/FileTransfer.jsx', import.meta.url), 'utf8')
|
|
.replace(/^import\s[^;]*;\s*$/gm, '');
|
|
vm.runInNewContext(source, context);
|
|
|
|
const manager = {
|
|
fileTransferSystem: {
|
|
onProgress: () => {},
|
|
onFileReceived: () => {},
|
|
onError: () => {},
|
|
onIncomingFileRequest: () => {}
|
|
},
|
|
setFileTransferCallbacks(...args) {
|
|
callbackCalls.push(args);
|
|
this.onFileProgress = args[0];
|
|
this.onFileReceived = args[1];
|
|
this.onFileError = args[2];
|
|
this.onIncomingFileRequest = args[3];
|
|
if (this.fileTransferSystem) {
|
|
this.fileTransferSystem.onProgress = args[0];
|
|
this.fileTransferSystem.onFileReceived = args[1];
|
|
this.fileTransferSystem.onError = args[2];
|
|
this.fileTransferSystem.onIncomingFileRequest = args[3];
|
|
}
|
|
},
|
|
getFileTransfers() {
|
|
return { sending: [], receiving: [] };
|
|
},
|
|
isConnected() {
|
|
return false;
|
|
},
|
|
isVerified: false
|
|
};
|
|
|
|
// Component no longer manages callbacks — consent is handled by the parent (app.jsx).
|
|
// pendingIncomingFiles and onIncomingDecision are passed as props.
|
|
context.window.FileTransferComponent({ webrtcManager: manager, isConnected: false, pendingIncomingFiles: [], onIncomingDecision: null });
|
|
const cleanups = effects.map(effect => effect()).filter(Boolean);
|
|
|
|
// State index 0 = dragOver, index 1 = transfers.
|
|
// Transfers state should be reset to empty on disconnect.
|
|
assert.ok(setterCalls.some(call => call.index === 1 && call.value.sending.length === 0 && call.value.receiving.length === 0));
|
|
|
|
// Component must NOT call setFileTransferCallbacks — that is the parent's responsibility.
|
|
assert.equal(callbackCalls.length, 0, 'FileTransferComponent must not register its own callbacks');
|
|
|
|
// Cleanup effects must not null-out the manager's callbacks either.
|
|
cleanups.forEach(cleanup => cleanup());
|
|
assert.equal(callbackCalls.length, 0, 'cleanup must not call setFileTransferCallbacks');
|
|
|
|
// fileTransferSystem callbacks are untouched by the component.
|
|
assert.equal(typeof manager.fileTransferSystem.onProgress, 'function');
|
|
assert.equal(typeof manager.fileTransferSystem.onFileReceived, 'function');
|
|
assert.equal(typeof manager.fileTransferSystem.onError, 'function');
|
|
assert.equal(typeof manager.fileTransferSystem.onIncomingFileRequest, 'function');
|
|
|
|
console.log('File transfer UI cleanup tests passed');
|