Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .beads/sync_base.jsonl

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ src/.saves/

# Telemetry runtime output (prototype)
server/telemetry/events.ndjson

# Generated validation results
results/
11 changes: 11 additions & 0 deletions results/validate-test-story.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"story": "/home/rgardler/projects/GEngine/web/stories/test-story.ink",
"pass": false,
"steps": 0,
"path": [],
"error": "Parse/compile error: Compilation failed.",
"rotationOpportunity": false,
"exhausted": false
}
]
38 changes: 38 additions & 0 deletions tests/load-test-story.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from '@playwright/test';

async function collectConsoleErrors(page) {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
return errors;
}

test('demo loads test-story.ink via ?story and presents UI', async ({ page }) => {
const errors = await collectConsoleErrors(page);

// Load the demo and request the test story explicitly
await page.goto('/demo/?story=/stories/test-story.ink');

// Ensure the demo actually fetched the specific story file
await page.waitForResponse(resp => resp.url().endsWith('/stories/test-story.ink') && resp.status() === 200, { timeout: 5000 });

const story = page.locator('#story');
await expect(story).toBeVisible();

// Wait until the story area has text content
await page.waitForFunction(() => {
const el = document.querySelector('#story');
return !!el && el.textContent && el.textContent.trim().length > 0;
}, undefined, { timeout: 5000 });

const choices = page.locator('.choice-btn');
await expect.poll(async () => choices.count(), { timeout: 5000 }).toBeGreaterThan(0);

// Verify rendered content is from the test story (not the default demo)
const rendered = await story.textContent();
await expect(rendered).toContain('They are keeping me waiting.');

// No console errors
expect(errors, 'Console errors should be empty').toEqual([]);
});
28 changes: 27 additions & 1 deletion web/demo/js/inkrunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,21 @@
const intensityInput = document.getElementById('smoke-intensity');
const SAVE_KEY = 'ge-hch.smoke.save';
// Support GitHub Pages project path prefixes (e.g., /GEngine/demo/)
const STORY_PATH = `${window.location.pathname.split('/demo')[0] || ''}/stories/demo.ink`;
// Allow selecting a story via the `?story=` query parameter. If provided,
// the param should be a site-root-relative path (eg. `/stories/test-story.ink`).
const _basePrefix = window.location.pathname.split('/demo')[0] || '';
const _params = new URLSearchParams(window.location.search || '');
const _storyParam = _params.get('story');
let STORY_PATH;
if (_storyParam) {
// If the provided param is absolute (starts with '/'), respect it and prefix
// with the repository base path to support project pages. Otherwise treat
// it as site-root-relative by adding a leading '/'.
const normalized = _storyParam.startsWith('/') ? _storyParam : `/${_storyParam}`;
STORY_PATH = `${_basePrefix}${normalized}`;
} else {
STORY_PATH = `${_basePrefix}/stories/demo.ink`;
}

let story;
const mockProposalQueue = [];
Expand Down Expand Up @@ -448,23 +462,35 @@
console.error('InkJS Compiler missing; ensure vendor/ink.js is the ink-full build and server serves /demo/vendor/ink.js fresh.');
return;
}
// Diagnostic logging: show which story path we're about to fetch
try { console.log('[inkrunner] loadStory: computed STORY_PATH ->', STORY_PATH); } catch(e) {}
let source;
try {
const res = await fetch(STORY_PATH, { cache: 'no-cache' });
// Log fetch diagnostics
try { console.log('[inkrunner] fetch', STORY_PATH, 'status', res.status, 'content-type', res.headers.get('content-type')); } catch(e) {}
if (!res.ok) {
console.error(`Failed to fetch Ink story at ${STORY_PATH} (status ${res.status}). Serve from repo root or web/.`);
return;
}
source = await res.text();
try { console.log('[inkrunner] fetched source preview:\n', source.slice(0,400).replace(/\n/g,'\\n')); } catch(e) {}
} catch (err) {
console.error(`Failed to fetch Ink story at ${STORY_PATH}`, err);
return;
}
try {
const compiled = new inkjs.Compiler(source).Compile();
story = compiled instanceof inkjs.Story ? compiled : new inkjs.Story(compiled);
try { console.log('[inkrunner] compiled story OK, story object set:', !!story); } catch(e) {}
} catch (err) {
console.error('Failed to load Ink story', err, { usingSource: 'fetched', protocol: window.location.protocol });
try {
if (err && err.compilerErrors) console.error('[inkrunner] compile exception.compilerErrors:', err.compilerErrors);
if (typeof inkjs !== 'undefined' && inkjs.Compiler && inkjs.Compiler._errors) {
console.error('[inkrunner] compiler._errors:', inkjs.Compiler._errors);
}
} catch (_){ }
return;
}

Expand Down
33 changes: 28 additions & 5 deletions web/demo/js/player-preference.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,48 @@
}
}

// In-memory cache to avoid repeated JSON.parse on hot paths.
// We still detect external changes to localStorage by comparing the raw string.
let _cachedRaw = null;
let _cachedState = null;

function loadState() {
if (typeof localStorage === 'undefined') return { events: [], totals: {} };
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { events: [], totals: {} };
// If nothing stored, normalize to null for comparison
const normRaw = raw || null;
if (_cachedRaw === normRaw && _cachedState) return _cachedState;
if (!raw) {
_cachedRaw = null;
_cachedState = { events: [], totals: {} };
return _cachedState;
}
const parsed = safeParse(raw);
if (!parsed || typeof parsed !== 'object') return { events: [], totals: {} };
if (!parsed || typeof parsed !== 'object') {
_cachedRaw = null;
_cachedState = { events: [], totals: {} };
return _cachedState;
}
const events = Array.isArray(parsed.events) ? parsed.events : [];
const totals = parsed.totals && typeof parsed.totals === 'object' ? parsed.totals : {};
return { events, totals };
_cachedRaw = normRaw;
_cachedState = { events, totals };
return _cachedState;
} catch (_) {
return { events: [], totals: {} };
_cachedRaw = null;
_cachedState = { events: [], totals: {} };
return _cachedState;
}
}

function saveState(state) {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
const raw = JSON.stringify(state);
localStorage.setItem(STORAGE_KEY, raw);
_cachedRaw = raw || null;
_cachedState = state;
} catch (_) {
// ignore write errors
}
Expand Down
2 changes: 2 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ <h1>GEngine Demo</h1>
<p>The GitHub Pages static demo is available at:</p>
<ul>
<li><a href="./demo/">/demo/</a></li>
<!-- Open the story inside the demo app so it runs (not a raw file download) -->
<li><a href="./demo/?story=/stories/test-story.ink">/demo/?story=/stories/test-story.ink</a></li>
</ul>
<p>If you reached this page from the repo root, use the link above to load the InkJS demo.</p>
</div>
Expand Down
Loading