Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/screenshots/01-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/02-start-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/03-terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/04-file-explorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/05-browser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/06-code-editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/07-system-monitor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/08-terminal-commands.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions docs/screenshots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Playwright Test Report – Hacker Simulator (JS app)

The web application (`/src`) was built and served locally, then driven end‑to‑end
with Playwright (headless Chrome). Every desktop application, the Start menu and
the terminal command processor were exercised. The bugs found during testing were
fixed; after the fixes the app loads and runs with **no application console/page
errors** (the only remaining console messages are external CDN resources –
Google Fonts and the xterm stylesheet – which fail only in a fully offline
sandbox and degrade gracefully).

## Issues found and fixed

| # | Area | Problem | Fix |
|---|------|---------|-----|
| 1 | Build | `src/styles/main.less` imported Google Fonts via `@import url(...)`, which `less-loader` resolves at **build time** and fails offline, breaking the whole build. | Load the font at runtime with a `<link>` in `index.html`; removed the build‑time `@import`. |
| 2 | Startup | Start Menu read the IndexedDB‑backed filesystem in its constructor (before `fileSystem.init()`), throwing `Database not initialized` and an unhandled page error. | Defer `buildPinedApps()` to the existing `os.Ready()` callback, after the filesystem is initialized. |
| 3 | Code Editor | Monaco loaded its web worker from `https://unpkg.com/monaco-editor@latest/...`, which fails offline and risks a version mismatch with the bundled Monaco. | Use webpack‑bundled workers via `new Worker(new URL('monaco-editor/esm/...', import.meta.url))`. |
| 4 | Theme init | `ThemeManager` used `stat()` (which logs an error for a missing path) to test whether `/etc/themes` exists, logging an error on every startup. | Use `exists()` instead, treating a missing directory as an expected condition. |
| 5 | Page metadata | The app shipped no favicon, so every load produced a `GET /favicon.ico 404`. | Added an inline SVG favicon (no external file, works offline). |
| 6 | Blazor build | Stray extra `}` in `WindowStateTest.razor.cs` broke the `wasm2` Blazor build. | Removed the extra brace. |

## Screenshots

| View | File |
|------|------|
| Desktop | `01-desktop.png` |
| Start menu (pinned app tiles with colors) | `02-start-menu.png` |
| Terminal | `03-terminal.png` |
| File Explorer | `04-file-explorer.png` |
| Browser (in‑game HackerSearch) | `05-browser.png` |
| Code Editor (Monaco, locally bundled) | `06-code-editor.png` |
| System Monitor | `07-system-monitor.png` |
| Terminal running `help` and `ls /` | `08-terminal-commands.png` |
33 changes: 22 additions & 11 deletions src/apps/code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,30 @@ export class CodeEditorApp extends GuiApplication {
private initMonaco(): void {
if (!this.editorContainer) return;

// Configure Monaco workers
// Configure Monaco workers to use the locally bundled worker files.
// webpack 5 bundles each `new Worker(new URL(...))` into its own chunk,
// so the editor works offline and always matches the bundled Monaco
// version (previously these were fetched from unpkg.com@latest, which
// failed offline and risked version mismatches).
(window as any).MonacoEnvironment = {
getWorker: function(_moduleId: string, label: string) {
// Use a proxied worker that doesn't rely on specific worker files
// This is a workaround when worker files aren't properly bundled
const workerContent = `
self.MonacoEnvironment = {
baseUrl: 'https://unpkg.com/monaco-editor@latest/min/'
};
importScripts('https://unpkg.com/monaco-editor@latest/min/vs/base/worker/workerMain.js');
`;
const blob = new Blob([workerContent], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
switch (label) {
case 'json':
return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url));
case 'css':
case 'scss':
case 'less':
return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url));
case 'html':
case 'handlebars':
case 'razor':
return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url));
case 'typescript':
case 'javascript':
return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url));
default:
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url));
}
}
};

Expand Down
11 changes: 5 additions & 6 deletions src/core/ThemeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,12 @@ export class ThemeManager {
* Make sure the themes directory exists
*/
private async ensureThemesDirectory(): Promise<void> {
try {
// Check if directory exists
await this.fileSystem.stat(this.themesDirectory);
} catch (error) {
// Directory doesn't exist, create it
// Use exists() rather than stat() so a missing directory is treated as an
// expected condition instead of logging an error. The directory is created
// on first run when it does not yet exist.
const exists = await this.fileSystem.exists(this.themesDirectory);
if (!exists) {
await this.fileSystem.createDirectory(this.themesDirectory, null, true);

}
}

Expand Down
22 changes: 16 additions & 6 deletions src/core/start-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,16 @@ export class StartMenuController {



this.os.Ready(() => {
// Load pinned apps from user settings
this.loadPinnedApps();
this.os.Ready(async () => {
// Load pinned apps from user settings. This reads from the virtual
// filesystem (IndexedDB), so it must only run once the OS – and in
// particular the filesystem – has finished initializing.
await this.loadPinnedApps();

// Build the pinned apps view now that the filesystem is ready. This is
// intentionally not done in the constructor to avoid accessing the
// database before it is initialized.
await this.buildPinedApps();

// Initialize Lucide icons
this.initIcons();
Expand Down Expand Up @@ -142,10 +149,11 @@ export class StartMenuController {
}
}

// If we have pinned apps, use them, otherwise keep the default
// If we have pinned apps, use them, otherwise keep the default.
// The actual rendering (buildPinedApps) is performed by the caller
// once loading is complete, so we only update the config here.
if (newTileConfig.length > 0) {
this.appTileConfig = newTileConfig;
this.buildPinedApps();
}
}
}
Expand Down Expand Up @@ -215,7 +223,9 @@ export class StartMenuController {
const pinnedApps = document.createElement('div');
pinnedApps.className = 'pinned-apps'; // Add app tiles
this.pinnedAppsEl = pinnedApps;
this.buildPinedApps();
// NOTE: Pinned apps are built later, once the OS/filesystem is ready
// (see the os.Ready callback in the constructor). Building them here would
// access the IndexedDB-backed filesystem before it is initialized.

pinnedAppsView.appendChild(pinnedApps);
content.appendChild(pinnedAppsView);
Expand Down
4 changes: 4 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hacker Simulator</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230d1117'/%3E%3Cpath d='M3 4.5l3 3-3 3' fill='none' stroke='%2300ff9c' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline x1='8' y1='11' x2='12' y2='11' stroke='%2300ff9c' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">

</head>
Expand Down
4 changes: 3 additions & 1 deletion src/styles/main.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Main LESS file that imports all other style components
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
// Note: The "Inter" web font is loaded at runtime via a <link> tag in index.html.
// It is intentionally not imported here so the build does not depend on fetching
// a remote resource (which fails in offline/sandboxed environments).
@import "variables.less";
@import "base.less";
@import "taskbar.less";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,3 @@ protected override void OnAfterRender(bool firstRender)
}
}
}
}