432 lines
33 KiB
JavaScript
432 lines
33 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import vm from "node:vm";
|
|
const worker = await import(new URL("../dist/server/index.js", import.meta.url));
|
|
assert.equal(typeof worker.default?.fetch, "function");
|
|
const assets = await import(new URL("../dist/server/assets.js", import.meta.url));
|
|
const source = await readFile(new URL("../public/index.html", import.meta.url), "utf8");
|
|
assert.match(assets.indexHtml, /!\[1,2\]\.includes\(d\.schemaVersion\)/);
|
|
assert.doesNotMatch(assets.indexHtml, /localizeDynamicText|createTreeWalker|NodeFilter\.SHOW_TEXT|const ZH=/);
|
|
assert.match(assets.indexHtml, /window\.LifeOSLocales\.en=/);
|
|
assert.match(assets.addonsJs, /window\.exportData = exportData/);
|
|
assert.match(assets.addonsJs, /schemaVersion: 2/);
|
|
assert.match(assets.addonsJs, /settings: settings\(\), system: sys\(\)/);
|
|
assert.match(assets.addonsJs, /widgetLayout/);
|
|
assert.match(assets.i18nRenderersJs, /lifeos_widget_layout_v1/);
|
|
assert.match(assets.addonsJs, /lifeos_cloud_baseline_v3/);
|
|
assert.match(assets.addonsJs, /api\/lifeos-sync/);
|
|
assert.match(assets.i18nRenderersJs, /dialog\.goals/);
|
|
assert.match(assets.i18nRenderersJs, /shell\.wallpaperTitle/);
|
|
assert.match(assets.i18nRenderersJs, /window\.museumApp/);
|
|
assert.equal(assets.manifest.includes('"description"'), false);
|
|
const flatten = (value, prefix = "") => Object.entries(value).flatMap(([key, child]) => child && typeof child === "object" && !Array.isArray(child) ? flatten(child, prefix ? `${prefix}.${key}` : key) : [prefix ? `${prefix}.${key}` : key]).sort();
|
|
const enKeys = flatten(assets.localeEn), zhKeys = flatten(assets.localeZh);
|
|
assert.deepEqual(enKeys, zhKeys, "English and Chinese locale keys must match");
|
|
const resolveKey = (locale, key) => key.split(".").reduce((value, part) => value?.[part], locale);
|
|
for (const script of [assets.i18nRenderersJs, assets.desktopPersonalizationJs, assets.desktopWidgetsJs, assets.addonsJs, source]) {
|
|
for (const match of script.matchAll(/(?:\bt|LifeI18n\.t)\(['"`]([A-Za-z][\w.]+)['"`]/g)) {
|
|
assert.notEqual(resolveKey(assets.localeEn, match[1]), undefined, `Missing locale key: ${match[1]}`);
|
|
assert.notEqual(resolveKey(assets.localeZh, match[1]), undefined, `Missing Chinese locale key: ${match[1]}`);
|
|
}
|
|
}
|
|
for (const locale of [assets.localeEn, assets.localeZh]) {
|
|
assert.ok(locale.login.title && locale.login.description && locale.login.continue && locale.login.footnote);
|
|
assert.equal(locale.prompt.films.length, 30);
|
|
assert.equal(locale.prompt.lines.length, 30);
|
|
}
|
|
for (const script of [assets.tasksJs, assets.calendarJs]) {
|
|
assert.doesNotMatch(script, /createTreeWalker|NodeFilter\.SHOW_TEXT/);
|
|
}
|
|
for (const [, inlineScript] of source.matchAll(/<script>([\s\S]*?)<\/script>/g)) new vm.Script(inlineScript);
|
|
assert.doesNotMatch(source, /localizeDynamicText|createTreeWalker|NodeFilter\.SHOW_TEXT|const ZH=/);
|
|
assert.match(source, /id==='system'\)window\.systemApp\(c\)/);
|
|
assert.match(source, /id==='today'\)window\.today\(c\)/);
|
|
assert.match(source, /id==='browser'\)window\.browserApp\(c\)/);
|
|
assert.match(source, /id==='terminal'\)window\.terminalApp\(c\)/);
|
|
assert.match(assets.i18nJs, /window\.setLanguage = setLanguage/);
|
|
assert.match(assets.i18nRenderersJs, /window\.renderAll\(\)/);
|
|
assert.match(assets.indexHtml, /setTimeout\(\(\)=>window\.applyLanguage\?\.\(\),0\)/, "missing locale UI code cannot abort desktop initialization");
|
|
assert.doesNotMatch(assets.indexHtml, /setTimeout\(applyLanguage\s*,/);
|
|
assert.doesNotMatch(assets.addonsJs, /(?<![\w.])applyLanguage\s*\(/, "add-on actions must not call a missing global locale function");
|
|
assert.match(assets.indexHtml, /window\.renderIcons=renderIcons;window\.renderAll=renderAll/, "shared renderers are explicitly exposed to locale scripts");
|
|
const shippedScripts = [...assets.indexHtml.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(match => match[1]);
|
|
assert.equal(shippedScripts.length, 16, "the complete desktop script chain is present in the served HTML");
|
|
assert.doesNotMatch(assets.indexHtml, /<script\s+src=/, "the desktop cannot silently fall back when an app script fails to load");
|
|
assert.ok(assets.indexHtml.indexOf('window.systemApp =') < assets.indexHtml.lastIndexOf("window.init()"), "locale renderers run before desktop initialization");
|
|
for (const script of shippedScripts) new vm.Script(script);
|
|
for (const script of [assets.i18nJs, assets.dataActionsJs, assets.terminalAppJs, assets.dbAdminJs, assets.tasksJs, assets.calendarJs, assets.i18nRenderersJs, assets.desktopPersonalizationJs, assets.desktopWidgetsJs, assets.browserAppJs, assets.addonsJs]) new vm.Script(script);
|
|
const adminNode = { innerHTML: "", querySelector: () => ({ addEventListener() {} }) };
|
|
const adminContext = vm.createContext({
|
|
document: { createElement: () => ({ textContent: "" }), head: { append() {} }, querySelector: () => adminNode },
|
|
LifeI18n: { t: key => key }, window: {}, Date
|
|
});
|
|
vm.runInContext(assets.dbAdminJs, adminContext);
|
|
adminContext.window.databaseApp(adminNode);
|
|
assert.match(adminNode.innerHTML, /lifeos-admin/);
|
|
assert.match(adminNode.innerHTML, /type="password"/);
|
|
assert.match(adminNode.innerHTML, /name="acknowledged" required/);
|
|
const browserNodes = Object.fromEntries(['.browser-toolbar', '.browser-address', 'iframe', '[data-action="back"]', '[data-action="forward"]', '.browser-external', '.browser-hint-link'].map(key => [key, { value: '', href: '', disabled: false, listeners: {}, addEventListener(type, handler) { this.listeners[type] = handler; }, getAttribute(name) { return this[name] ?? null; } }]));
|
|
const browserContainer = { dataset: {}, innerHTML: '', querySelector(selector) { return selector === '.browser-shell' ? (this.innerHTML ? {} : null) : browserNodes[selector]; } };
|
|
const browserMessages = [];
|
|
const browserContext = vm.createContext({ document: { activeElement: null, querySelector: () => browserContainer, createElement: () => ({}), head: { append() {} } }, LifeI18n: { t: key => key }, lang: () => 'en', toast: message => browserMessages.push(message), URL });
|
|
browserContext.window = browserContext;
|
|
vm.runInContext(assets.browserAppJs, browserContext);
|
|
browserContext.browserApp(browserContainer);
|
|
assert.equal(browserNodes.iframe.src, 'https://www.google.com/webhp?igu=1', 'the default embedded page is Google');
|
|
browserNodes['.browser-address'].value = 'example.org/path';
|
|
browserNodes['.browser-toolbar'].listeners.submit({ preventDefault() {} });
|
|
assert.equal(browserNodes.iframe.src, 'https://example.org/path');
|
|
assert.equal(browserNodes['.browser-hint-link'].href, 'https://example.org/path');
|
|
browserNodes['.browser-address'].value = 'javascript:alert(1)';
|
|
browserNodes['.browser-toolbar'].listeners.submit({ preventDefault() {} });
|
|
assert.equal(browserNodes.iframe.src, 'https://example.org/path', 'unsafe schemes do not navigate');
|
|
assert.equal(browserMessages.at(-1), 'browser.invalid');
|
|
browserNodes['.browser-toolbar'].listeners.click({ target: { closest: () => ({ dataset: { action: 'back' } }) } });
|
|
assert.equal(browserNodes.iframe.src, 'https://www.google.com/webhp?igu=1');
|
|
const terminalStorage = new Map(), terminalRenders = [];
|
|
const terminalContext = vm.createContext({
|
|
document: { createElement: () => ({ textContent: '' }), head: { append() {} }, querySelector: () => null },
|
|
console: { info() {}, warn() {} },
|
|
LifeI18n: { t: key => key, dictionaries: { en: assets.localeEn, zh: assets.localeZh } }, lang: () => 'en',
|
|
APPS: { calendar: ['icon', 'CALENDAR'] }, openApp: id => terminalRenders.push(id),
|
|
crypto: globalThis.crypto, day: () => '2026-09-28',
|
|
mems: () => terminalStorage.get('lifeos_memories') || [],
|
|
museum: () => terminalStorage.get('lifeos_museum') || [],
|
|
sys: () => terminalStorage.get('lifeos_system') || { schemaVersion: 2 },
|
|
save: (key, value) => terminalStorage.set(key, structuredClone(value)),
|
|
renderAll: () => terminalRenders.push('render'),
|
|
filmFor: () => ({ filmTitle: 'Prompt', filmSubtitle: '', filmLine: '' })
|
|
});
|
|
terminalContext.window = terminalContext;
|
|
vm.runInContext(assets.dataActionsJs, terminalContext);
|
|
vm.runInContext(assets.terminalAppJs, terminalContext);
|
|
const command = terminalContext.LifeOSTerminalEngine.run;
|
|
assert.equal(command('goal add "Sixth form" --due 2026-12-12 --pin').message, 'terminal.goalSaved');
|
|
assert.equal(command('task add "Review words" --goal "Sixth form" --priority high').message, 'terminal.taskSaved');
|
|
assert.equal(command('task done 1').message, 'terminal.taskCompleted');
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks[0].done, true);
|
|
assert.equal(command('memory add "Experiment" --date 2026-09-28 --tag school --energy 4 --text "Finished lab"').message, 'terminal.memorySaved');
|
|
assert.equal(terminalStorage.get('lifeos_memories')[0].text, 'Finished lab');
|
|
assert.equal(terminalStorage.get('lifeos_memories')[0].tags[0], 'school');
|
|
assert.throws(() => command('memory add "Again" --text "Duplicate"'), /memoryExists/);
|
|
assert.throws(() => command('goal add "Invalid date" --due 2026-02-30'), /invalidDate/);
|
|
assert.match(command('memory add "Multiline" --date 2026-09-29').message, /terminal\.wizard\.tags/);
|
|
assert.match(command('back').message, /terminal\.wizard\.date/);
|
|
assert.match(command('2026-09-29').message, /terminal\.wizard\.tags/);
|
|
assert.equal(command('cancel').cancel, true);
|
|
assert.equal(command('memory add').message.includes('terminal.wizard.title'), true);
|
|
assert.match(command('A second memory').message, /terminal\.wizard\.date/);
|
|
assert.match(command('2026-09-30').message, /terminal\.wizard\.tags/);
|
|
assert.match(command('school, lab').message, /terminal\.wizard\.energy/);
|
|
assert.match(command('5').message, /terminal\.wizard\.mood/);
|
|
assert.match(command('rain').message, /terminal\.wizard\.location/);
|
|
assert.match(command('Campus').message, /terminal\.wizard\.taskId/);
|
|
const wizardDraft = command('1').compose;
|
|
assert.equal(wizardDraft.title, 'A second memory');
|
|
assert.equal(wizardDraft.taskId, terminalStorage.get('lifeos_system').tasksBoard.tasks[0].id);
|
|
terminalContext.LifeOSActions.createMemory({ ...wizardDraft, text: 'Multiline body' });
|
|
terminalContext.LifeOSTerminalEngine.finishWizard();
|
|
assert.equal(terminalStorage.get('lifeos_memories').length, 2);
|
|
const terminalUiNodes = Object.fromEntries(['.terminal-output','.terminal-form','.terminal-compose','.terminal-input','.terminal-note','.terminal-shortcuts'].map(selector => [selector, { value: '', hidden: false, listeners: {}, children: [], focus() {}, addEventListener(type, handler) { this.listeners[type] = handler; }, replaceChildren() { this.children = []; }, append(child) { this.children.push(child); } }]));
|
|
const terminalUi = { innerHTML: '', dataset: {}, querySelector(selector) { return selector === '.terminal-shell' ? (this.innerHTML ? {} : null) : terminalUiNodes[selector]; } };
|
|
terminalContext.terminalApp(terminalUi);
|
|
assert.equal(terminalUiNodes['.terminal-output'].children[0].textContent, 'terminal.welcome', 'opening the terminal shows its welcome banner');
|
|
const submitTerminal = value => { terminalUiNodes['.terminal-input'].value = value; terminalUiNodes['.terminal-form'].listeners.submit({ preventDefault() {} }); };
|
|
submitTerminal('memory add "Via UI" --date 2026-10-01');
|
|
for (let index = 0; index < 5; index++) submitTerminal('');
|
|
assert.equal(terminalUiNodes['.terminal-compose'].hidden, false, 'guided memory input reaches the multiline note');
|
|
terminalUiNodes['.terminal-note'].value = 'A note from the guided form';
|
|
terminalUiNodes['.terminal-compose'].listeners.click({ target: { closest: () => ({ dataset: { note: 'save' } }) } });
|
|
assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-01').text, 'A note from the guided form');
|
|
assert.equal(terminalUiNodes['.terminal-compose'].hidden, true);
|
|
submitTerminal('goal add "UI goal"');
|
|
submitTerminal(''); submitTerminal(''); submitTerminal('');
|
|
assert.match(terminalUiNodes['.terminal-output'].children.at(-1).textContent, /terminal\.flow\.confirmCreate/);
|
|
submitTerminal('yes');
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.goals[0].title, 'UI goal');
|
|
submitTerminal('goal delete "UI goal"');
|
|
submitTerminal('yes');
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.goals.some(item => item.title === 'UI goal'), false);
|
|
assert.match(command('goal add').message, /terminal\.flow\.title/);
|
|
assert.match(command('A guided goal').message, /terminal\.flow\.description/);
|
|
assert.match(command('').message, /terminal\.flow\.targetDate/);
|
|
assert.match(command('2027-01-02').message, /terminal\.flow\.showOnDesktop/);
|
|
assert.match(command('yes').message, /terminal\.flow\.confirmCreate/);
|
|
assert.equal(command('yes').message, 'terminal.goalSaved');
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.goals[0].showOnDesktop, true);
|
|
assert.match(command('task add').message, /terminal\.flow\.title/);
|
|
assert.match(command('A guided task').message, /terminal\.flow\.dueDate/);
|
|
assert.match(command('').message, /terminal\.flow\.goalId/);
|
|
assert.match(command('1').message, /terminal\.flow\.priority/);
|
|
assert.match(command('high').message, /terminal\.flow\.confirmCreate/);
|
|
assert.equal(command('yes').message, 'terminal.taskSaved');
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks[1].goalId, terminalStorage.get('lifeos_system').tasksBoard.goals[0].id);
|
|
assert.match(command('task delete').message, /terminal\.flow\.pick/);
|
|
assert.match(command('2').message, /terminal\.flow\.confirmDelete/);
|
|
assert.equal(command('no').cancel, true);
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks.length, 2);
|
|
assert.match(command('task delete 2').message, /terminal\.flow\.confirmDelete/);
|
|
assert.match(command('yes').message, /terminal\.flow\.deleted/);
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks.length, 1);
|
|
assert.match(command('memory delete 2026-09-30').message, /terminal\.flow\.confirmDelete/);
|
|
assert.equal(command('yes').message.includes('terminal.flow.deleted'), true);
|
|
assert.equal(terminalStorage.get('lifeos_memories').some(item => item.date === '2026-09-30'), false);
|
|
terminalContext.LifeOSActions.createTask({ title: 'Linked task', goalId: terminalStorage.get('lifeos_system').tasksBoard.goals[0].id });
|
|
assert.match(command('goal delete 1').message, /terminal\.flow\.confirmDelete/);
|
|
assert.match(command('yes').message, /terminal\.flow\.deleted/);
|
|
assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks[1].goalId, '');
|
|
terminalContext.LifeOSActions.createMemory({ date: '2026-10-02', title: 'Linked memory', taskId: terminalStorage.get('lifeos_system').tasksBoard.tasks[0].id });
|
|
assert.match(command('task delete 1').message, /terminal\.flow\.confirmDelete/);
|
|
assert.match(command('yes').message, /terminal\.flow\.deleted/);
|
|
assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-02').taskId, '');
|
|
assert.ok(command('date').message.length > 8);
|
|
assert.ok(command('time').message.includes(':'));
|
|
assert.ok(command('datetime').message.length > 12);
|
|
assert.equal(command('uptime').message, 'terminal.uptime');
|
|
assert.equal(command('version').message, 'Life OS v1.14.0');
|
|
assert.equal(command('status').message, 'terminal.status');
|
|
assert.match(command('apps').message, /calendar/);
|
|
assert.equal(command('echo "hello Life OS"').message, 'hello Life OS');
|
|
assert.match(terminalContext.LifeOS.help(), /memory add/);
|
|
assert.match(terminalContext.LifeOS.run('goal add'), /terminal\.flow\.title/);
|
|
assert.equal(terminalContext.LifeOS.cancel(), 'terminal.flow.cancelled');
|
|
assert.match(terminalContext.LifeOS.run('memory add "Console memory" --date 2026-10-03'), /terminal\.wizard\.tags/);
|
|
for (let index = 0; index < 5; index++) terminalContext.LifeOS.run('');
|
|
assert.equal(terminalContext.LifeOS.note('Created through DevTools'), 'terminal.memorySaved');
|
|
assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-03').text, 'Created through DevTools');
|
|
assert.equal(terminalContext.LifeOS.note('Again'), 'terminal.noNote');
|
|
assert.equal(terminalContext.LifeOS.run('window.alert(1)'), 'terminal.unknownCommand');
|
|
assert.equal(command('open calendar').message, 'terminal.opened');
|
|
assert.ok(terminalRenders.includes('calendar'));
|
|
assert.deepEqual(Array.from(terminalContext.LifeOSTerminalEngine.words('task add "quoted title" --priority high')), ['task','add','quoted title','--priority','high']);
|
|
assert.doesNotMatch(source, /Version 0\.1|Schema v1/);
|
|
const makeStorage = initial => {
|
|
const values = new Map(Object.entries(initial || {}));
|
|
return { getItem: key => values.get(key) ?? null, setItem: (key,value) => values.set(key,String(value)), values };
|
|
};
|
|
let legacySettings = { language: "auto", languageMode: "auto" };
|
|
const calls = { icons: 0, render: 0, apply: 0, toast: 0 };
|
|
const localStorage = makeStorage();
|
|
const localeContext = vm.createContext({
|
|
navigator: { languages: ["zh-CN"], language: "zh-CN" },
|
|
localStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
settings: () => legacySettings,
|
|
renderIcons() { calls.icons++; }, renderAll() { calls.render++; }, applyLanguage() { calls.apply++; }, toast() { calls.toast++; },
|
|
});
|
|
localeContext.window = localeContext;
|
|
vm.runInContext(assets.i18nJs, localeContext);
|
|
assert.equal(localeContext.LifeI18n.preference(), "auto");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "auto mode follows Chinese browser language");
|
|
localeContext.navigator.languages = ["en-US"];
|
|
assert.equal(localeContext.LifeI18n.language(), "en", "auto mode follows English browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "TODAY");
|
|
localeContext.setLanguage("zh");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "manual Chinese choice overrides browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "今日");
|
|
assert.equal(localStorage.getItem("lifeos_language_preference_v1"), "zh");
|
|
assert.deepEqual(calls, { icons: 1, render: 1, apply: 1, toast: 1 }, "language selection redraws the desktop and shows feedback");
|
|
legacySettings = { language: "en", languageMode: "manual" };
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "cloud settings cannot overwrite the local manual choice");
|
|
localeContext.setLanguage("en");
|
|
assert.equal(localeContext.LifeI18n.language(), "en", "manual English choice overrides browser language");
|
|
assert.equal(localeContext.LifeI18n.t("app.today"), "TODAY");
|
|
const reloadedManualContext = vm.createContext({
|
|
navigator: { languages: ["en-US"] }, localStorage: makeStorage(),
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
});
|
|
reloadedManualContext.window = reloadedManualContext;
|
|
vm.runInContext(assets.i18nJs, reloadedManualContext);
|
|
assert.equal(reloadedManualContext.LifeI18n.language(), "en", "a reloaded page starts in automatic mode when no manual choice was saved");
|
|
localeContext.navigator.languages = ["zh-CN"];
|
|
localeContext.setLanguage("auto");
|
|
assert.equal(localeContext.LifeI18n.preference(), "auto");
|
|
assert.equal(localeContext.LifeI18n.language(), "zh", "auto mode ignores stale cloud preference and follows this device");
|
|
localeContext.setLanguage("en");
|
|
const persistedManualContext = vm.createContext({
|
|
navigator: { languages: ["zh-CN"] }, localStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
});
|
|
persistedManualContext.window = persistedManualContext;
|
|
vm.runInContext(assets.i18nJs, persistedManualContext);
|
|
assert.equal(persistedManualContext.LifeI18n.language(), "en", "a manually selected language persists across reloads");
|
|
const migratedStorage = makeStorage({ lifeos_settings: JSON.stringify({ language: "zh" }) });
|
|
const migratedContext = vm.createContext({
|
|
navigator: { languages: ["en-US"] }, localStorage: migratedStorage,
|
|
LifeOSLocales: { en: assets.localeEn, "zh-CN": assets.localeZh },
|
|
settings: () => ({ language: "zh", languageMode: "manual" }),
|
|
});
|
|
migratedContext.window = migratedContext;
|
|
vm.runInContext(assets.i18nJs, migratedContext);
|
|
assert.equal(migratedContext.LifeI18n.language(), "zh", "legacy manual preference migrates to the local preference key");
|
|
assert.match(assets.i18nRenderersJs, /preference === 'zh'/);
|
|
assert.match(assets.i18nRenderersJs, /onclick=\"window\.setLanguage\('zh'\)\"/);
|
|
assert.doesNotMatch(assets.addonsJs, /lifeos_language_preference_v1/, "the device locale preference stays outside cloud-synced keys");
|
|
const uiNodes = new Map();
|
|
const uiNode = () => ({ innerHTML: "", textContent: "", style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, remove() {}, append() {}, querySelector: () => null, querySelectorAll: () => [], addEventListener() {} });
|
|
for (const id of ["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal", "windows", "clock", "date", "desktop-brand", "life-launcher", "content-system", "content-insights"]) uiNodes.set(`#${id}`, uiNode());
|
|
const systemNode = uiNodes.get("#content-system");
|
|
systemNode.querySelector = selector => selector === ".settings" ? { insertAdjacentHTML: (_, markup) => { systemNode.innerHTML += markup; } } : null;
|
|
const uiDocument = {
|
|
documentElement: { dataset: {}, lang: "en" }, title: "", head: { insertAdjacentHTML() {}, append() {} }, body: { append() {} },
|
|
querySelector: selector => uiNodes.get(selector) || null, getElementById: id => uiNodes.get(`#${id}`) || null,
|
|
createElement: uiNode, addEventListener() {}, querySelectorAll: () => []
|
|
};
|
|
const uiStorage = makeStorage();
|
|
const uiContext = vm.createContext({
|
|
document: uiDocument, navigator: { languages: ["en-US"], language: "en-US" }, localStorage: uiStorage,
|
|
Blob, URL, crypto: globalThis.crypto, innerWidth: 1000, innerHeight: 800, addEventListener() {},
|
|
matchMedia: () => ({ matches: false }), setTimeout: () => 0, setInterval: () => 0,
|
|
fetch: async () => ({ ok: false, json: async () => ({}) }), console,
|
|
...Object.fromEntries(["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal"].map(id => [id, uiNodes.get(`#${id}`)]))
|
|
});
|
|
uiContext.window = uiContext;
|
|
for (const index of [0, 2, 3, 4, 5, 9, 10]) vm.runInContext(shippedScripts[index], uiContext);
|
|
vm.runInContext("windows.system={classList:{contains(){return false}}}", uiContext);
|
|
uiContext.setLanguage("zh");
|
|
assert.equal(uiContext.LifeI18n.language(), "zh");
|
|
assert.match(uiNodes.get("#content-system").innerHTML, /系统设置/);
|
|
assert.match(uiNodes.get("#content-system").innerHTML, /桌面个性化/);
|
|
assert.match(uiNodes.get("#icons").innerHTML, /今日/);
|
|
assert.equal(uiDocument.documentElement.lang, "zh-CN");
|
|
uiContext.setLanguage("en");
|
|
assert.match(uiNodes.get("#content-system").innerHTML, /SYSTEM SETTINGS/);
|
|
assert.match(uiNodes.get("#content-system").innerHTML, /DESKTOP PERSONALIZATION/);
|
|
assert.match(uiNodes.get("#icons").innerHTML, /TODAY/);
|
|
uiContext.navigator.languages = ["zh-CN"];
|
|
uiContext.setLanguage("auto");
|
|
assert.match(uiNodes.get("#content-system").innerHTML, /系统设置/);
|
|
assert.equal(uiContext.LifeI18n.preference(), "auto");
|
|
uiContext.setLanguage("en");
|
|
const calendarKey = date => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
const current = new Date(), previousWeek = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 7, 12);
|
|
const currentKey = calendarKey(current), previousKey = calendarKey(previousWeek), currentStamp = new Date(current.getFullYear(), current.getMonth(), current.getDate(), 12).toISOString();
|
|
uiStorage.setItem("lifeos_memories", JSON.stringify([
|
|
{ id: "current-highlight", date: currentKey, title: "Current highlight", text: "A real saved moment", mood: "clear", energy: 4, tags: ["高光"] },
|
|
{ id: "previous-highlight", date: previousKey, title: "Previous highlight", text: "An older moment", mood: "rain", energy: 2, tags: [] }
|
|
]));
|
|
uiStorage.setItem("lifeos_museum", JSON.stringify([{ id: "artifact-prev", memoryId: "previous-highlight" }]));
|
|
uiStorage.setItem("lifeos_system", JSON.stringify({ schemaVersion: 2, tasksBoard: {
|
|
tasks: [{ id: "done-now", done: true, createdAt: currentStamp, completedAt: currentStamp }, { id: "done-before", done: true, createdAt: previousWeek.toISOString(), completedAt: previousWeek.toISOString() }],
|
|
goals: [{ id: "goal-now", done: true, completedAt: currentStamp }]
|
|
} }));
|
|
uiContext.insights(uiNodes.get("#content-insights"));
|
|
let report = uiNodes.get("#content-insights").innerHTML;
|
|
assert.match(report, /Current highlight/);
|
|
assert.doesNotMatch(report, /Previous highlight/);
|
|
assert.match(report, /1 tasks created · 1 goals completed/);
|
|
assert.match(report, /First entry|One energy entry/);
|
|
uiContext.shiftInsights(-1);
|
|
report = uiNodes.get("#content-insights").innerHTML;
|
|
assert.match(report, /Previous highlight/);
|
|
assert.doesNotMatch(report, /Current highlight/);
|
|
assert.match(report, /SAVED/);
|
|
uiContext.setInsightsView("month");
|
|
report = uiNodes.get("#content-insights").innerHTML;
|
|
assert.match(report, /Current highlight/);
|
|
assert.match(report, /MONTH/);
|
|
uiStorage.setItem("lifeos_memories", "[]");
|
|
uiStorage.setItem("lifeos_museum", "[]");
|
|
uiStorage.setItem("lifeos_system", JSON.stringify({ schemaVersion: 2, tasksBoard: { tasks: [], goals: [] } }));
|
|
uiContext.insights(uiNodes.get("#content-insights"));
|
|
assert.match(uiNodes.get("#content-insights").innerHTML, /No weather recorded in this period/);
|
|
assert.match(uiNodes.get("#content-insights").innerHTML, /No saved or tagged memories in this period/);
|
|
uiContext.toggleDesktopApp("desktopShortcuts", "tasks");
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).desktopShortcuts.includes("tasks"), false, "removing a desktop shortcut preserves app access elsewhere");
|
|
uiContext.toggleDesktopApp("startupApps", "calendar");
|
|
assert.deepEqual(JSON.parse(uiStorage.getItem("lifeos_settings")).startupApps, ["calendar"]);
|
|
uiContext.setWallpaperSource("calm");
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).wallpaperSource, "calm");
|
|
assert.match(uiNodes.get("#desktop").style.backgroundImage, /radial-gradient/);
|
|
uiNodes.set("#desktop-wallpaper-url", { value: "javascript:alert(1)" });
|
|
uiContext.setWallpaperSource("custom");
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).wallpaperSource, "calm", "unsafe image URL is rejected");
|
|
uiNodes.get("#desktop-wallpaper-url").value = "https://example.com/photo.jpg";
|
|
uiContext.setWallpaperSource("custom");
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).wallpaperSource, "custom");
|
|
assert.match(uiNodes.get("#desktop").style.backgroundImage, /example\.com\/photo\.jpg/);
|
|
vm.runInContext("delete windows.system", uiContext);
|
|
uiContext.toggleWindowMemory();
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).rememberWindowLayout, false);
|
|
uiContext.toggleWindowMemory();
|
|
assert.equal(JSON.parse(uiStorage.getItem("lifeos_settings")).rememberWindowLayout, true);
|
|
const layoutStorage = makeStorage({
|
|
lifeos_settings: JSON.stringify({ wallpaperSource: "calm", startupApps: ["calendar"] }),
|
|
lifeos_window_layout_v1: JSON.stringify({ entries: [{ id: "tasks", left: 204, top: 137, width: 520, height: 410, max: false, min: true }] })
|
|
});
|
|
const layoutWindows = {}, layoutIcons = { addEventListener() {} }, layoutHost = { addEventListener() {} };
|
|
const layoutContext = vm.createContext({
|
|
APPS: { today: [], timeline: [], museum: [], insights: [], profile: [], tasks: [], calendar: [], system: [] },
|
|
windows: layoutWindows, localStorage: layoutStorage, innerWidth: 1200, innerHeight: 800,
|
|
document: { querySelector: selector => selector === "#icons" ? layoutIcons : selector === "#windows" ? layoutHost : { style: {} }, querySelectorAll: () => [], head: { append() {} }, createElement: () => ({ textContent: "" }) },
|
|
settings: () => ({ ...JSON.parse(layoutStorage.getItem("lifeos_settings")) }),
|
|
save: (key, value) => layoutStorage.setItem(key, JSON.stringify(value)),
|
|
LifeI18n: { t: key => key }, appName: id => id,
|
|
renderIcons() {}, systemApp() {}, applyWallpaper() {}, loadBingWallpaper() {}, init() {},
|
|
openApp(id) { const flags = new Set(); layoutWindows[id] = { dataset: { id }, style: { left: "20px", top: "20px" }, offsetWidth: 700, offsetHeight: 590, classList: { contains: key => flags.has(key), toggle: (key, state) => state ? flags.add(key) : flags.delete(key) } }; },
|
|
closeWin(id) { delete layoutWindows[id]; }, minWin() {}, maxWin() {}, focus() {}, renderTaskbar() {}, renderAll() {}, toast() {},
|
|
addEventListener() {}, setTimeout() {}
|
|
});
|
|
layoutContext.window = layoutContext;
|
|
vm.runInContext(assets.desktopPersonalizationJs, layoutContext);
|
|
layoutContext.init();
|
|
assert.deepEqual(Object.keys(layoutWindows), ["tasks", "calendar"], "remembered and startup apps both reopen once");
|
|
assert.equal(layoutWindows.tasks.style.left, "204px");
|
|
assert.equal(layoutWindows.tasks.style.top, "137px");
|
|
assert.equal(layoutWindows.tasks.classList.contains("min"), true);
|
|
layoutContext.closeWin("tasks");
|
|
assert.deepEqual(JSON.parse(layoutStorage.getItem("lifeos_window_layout_v1")).entries.map(entry => entry.id), ["calendar"], "closed windows stay closed on the next launch");
|
|
const widgetStorage = makeStorage({ lifeos_settings: "{}" });
|
|
widgetStorage.removeItem = key => widgetStorage.values.delete(key);
|
|
class WidgetNode {
|
|
constructor() { this.style = {}; this.dataset = {}; this.children = []; this.listeners = {}; this.flags = new Set(); this.classList = { add: key => this.flags.add(key), remove: key => this.flags.delete(key), contains: key => this.flags.has(key), toggle: (key, enabled) => enabled ? this.flags.add(key) : this.flags.delete(key) }; }
|
|
addEventListener(type, callback) { (this.listeners[type] ||= []).push(callback); }
|
|
removeEventListener(type, callback) { this.listeners[type] = (this.listeners[type] || []).filter(fn => fn !== callback); }
|
|
emit(type, event) { (this.listeners[type] || []).forEach(fn => fn(event)); }
|
|
append(node) { this.children.push(node); }
|
|
setAttribute() {}
|
|
setPointerCapture() {}
|
|
querySelector(selector) { if (selector === '.widget-empty-message') return null; return this[selector] ||= new WidgetNode(); }
|
|
insertAdjacentHTML() {}
|
|
}
|
|
const widgetDesktop = new WidgetNode(), widgetGoals = new WidgetNode();
|
|
const widgetContext = vm.createContext({
|
|
document: { querySelector: selector => selector === '#desktop' ? widgetDesktop : widgetGoals, createElement: () => new WidgetNode(), head: { append() {} } },
|
|
localStorage: widgetStorage, settings: () => JSON.parse(widgetStorage.getItem('lifeos_settings')),
|
|
save: (key, value) => widgetStorage.setItem(key, JSON.stringify(value)),
|
|
innerWidth: 1200, innerHeight: 800, addEventListener() {}, setInterval() {}, setTimeout() {}, clearTimeout() {},
|
|
renderDesktopGoalCountdowns() {}, renderAll() {}, systemApp() {}, openApp() {},
|
|
mems: () => [], sys: () => ({ tasksBoard: { tasks: [] } }), day: () => '2026-09-28',
|
|
LifeI18n: { t: key => key, locale: () => 'en-US', date: () => 'Sep 28', number: value => String(value) }
|
|
});
|
|
widgetContext.window = widgetContext;
|
|
vm.runInContext(assets.desktopWidgetsJs, widgetContext);
|
|
const widgetLayer = widgetDesktop.children[0], goalWidget = widgetLayer.children[0];
|
|
assert.equal(widgetLayer.children.length, 4, "goal, clock, today, and quick action widgets are available");
|
|
assert.match(widgetLayer.children[2].querySelector('.widget-body').innerHTML, /widgets\.memoriesToday/);
|
|
const goalHandle = goalWidget.querySelector('.widget-drag');
|
|
goalWidget.emit('pointerdown', { button: 0, target: { closest: selector => selector === '.widget-drag' ? goalHandle : null }, clientX: 0, clientY: 0, preventDefault() {} });
|
|
goalHandle.emit('pointermove', { clientX: -110, clientY: 98 });
|
|
goalHandle.emit('pointerup', {});
|
|
let savedWidget = JSON.parse(widgetStorage.getItem('lifeos_widget_layout_v1')).goals;
|
|
assert.equal(savedWidget.row, 1, "goal countdown moves by one visible grid cell");
|
|
const resizeHandle = goalWidget.querySelector('.widget-resize');
|
|
goalWidget.emit('pointerdown', { button: 0, target: { closest: selector => selector === '.widget-resize' ? resizeHandle : null }, clientX: 0, clientY: 0, preventDefault() {} });
|
|
resizeHandle.emit('pointermove', { clientX: -110, clientY: -98 });
|
|
resizeHandle.emit('pointerup', {});
|
|
savedWidget = JSON.parse(widgetStorage.getItem('lifeos_widget_layout_v1')).goals;
|
|
assert.equal(savedWidget.w, 2, "goal countdown width can be reduced");
|
|
assert.equal(savedWidget.h, 2, "goal countdown height can be reduced");
|
|
widgetContext.toggleDesktopWidget('actions');
|
|
assert.equal(JSON.parse(widgetStorage.getItem('lifeos_settings')).desktopWidgets.includes('actions'), true);
|
|
widgetContext.resetWidgetLayout();
|
|
assert.equal(widgetStorage.getItem('lifeos_widget_layout_v1'), null, "widget layout reset removes stored geometry");
|
|
const zhLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "zh-CN,zh;q=0.9" } }), {});
|
|
assert.match(await zhLogin.text(), /登录 ChatGPT/);
|
|
const enLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "en-US,en;q=0.9" } }), {});
|
|
assert.match(await enLogin.text(), /Sign in with ChatGPT/);
|
|
const localeResponse = await worker.default.fetch(new Request("https://life-os.test/locales/zh-CN.js"), {});
|
|
assert.equal(localeResponse.status, 200);
|
|
assert.match(await localeResponse.text(), /window\.LifeOSLocales\['zh-CN'\]/);
|
|
console.log(`Life OS artifact valid · ${enKeys.length} bilingual locale keys`);
|