fix: resolve remaining PR #1115 review items

Use AbortSignal.timeout for theme fetches, load themes without blocking
init, and add pageGeneration regression coverage for runner config changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jamesread
2026-09-10 11:07:37 +01:00
co-authored by Cursor
parent 1bf702da3e
commit a42b640331
8 changed files with 164 additions and 14 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"stylelint-config-standard": "^40.0.0"
},
"scripts": {
"test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs resources/vue/stores/*.test.mjs"
"test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs resources/vue/stores/*.test.mjs ../integration-tests/runner.test.mjs"
},
"author": "",
"parcelIgnore": [
+3 -1
View File
@@ -340,7 +340,9 @@ async function updateHeaderFromInit () {
loadCustomJsIfEnabled()
renderNavigation()
await applyTheme()
applyTheme().catch((err) => {
console.warn('Failed to load theme CSS:', err)
})
if (loginRequired.value) {
connectEventStreamIfNeeded()
+5 -12
View File
@@ -1,16 +1,6 @@
const DEFAULT_THEME_FETCH_TIMEOUT_MS = 5000
async function fetchWithTimeout (url, options, timeoutMs) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
try {
return await fetch(url, { ...options, signal: controller.signal })
} finally {
clearTimeout(timeoutId)
}
}
export async function applyThemeStyles (themePreference = '', timeoutMs = DEFAULT_THEME_FETCH_TIMEOUT_MS) {
export async function applyThemeStyles (themePreference = '') {
let themeStyle = document.getElementById('theme-style')
if (!themeStyle) {
@@ -24,7 +14,10 @@ export async function applyThemeStyles (themePreference = '', timeoutMs = DEFAUL
? `/custom-webui/themes/${encodeURIComponent(themePreference)}/theme.css`
: '/theme.css'
const response = await fetchWithTimeout(themeUrl, { cache: 'no-store' }, timeoutMs)
const response = await fetch(themeUrl, {
cache: 'no-store',
signal: AbortSignal.timeout(DEFAULT_THEME_FETCH_TIMEOUT_MS)
})
if (!response.ok) {
throw new Error(`theme fetch failed: ${response.status}`)
}
@@ -0,0 +1,57 @@
import test from 'node:test'
import assert from 'node:assert/strict'
function installThemeDom () {
const styleEl = { textContent: '' }
const body = { attributes: {}, setAttribute (name, value) { this.attributes[name] = value } }
globalThis.document = {
getElementById: (id) => (id === 'theme-style' ? styleEl : null),
createElement: () => styleEl,
head: { appendChild: () => {} },
body
}
return { styleEl, body }
}
test('applyThemeStyles times out when the response body stalls', async (t) => {
const originalFetch = globalThis.fetch
const originalDocument = globalThis.document
const originalAbortSignal = globalThis.AbortSignal
t.after(() => {
globalThis.fetch = originalFetch
globalThis.document = originalDocument
globalThis.AbortSignal = originalAbortSignal
})
installThemeDom()
globalThis.AbortSignal = {
timeout () {
const controller = new AbortController()
setTimeout(() => controller.abort(), 50)
return controller.signal
}
}
globalThis.fetch = (_url, options) => Promise.resolve({
ok: true,
status: 200,
text () {
return new Promise((_resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new DOMException('The operation was aborted.', 'AbortError'))
})
})
}
})
const { applyThemeStyles } = await import('./themeLoader.js')
await assert.rejects(
() => applyThemeStyles(''),
(err) => err.name === 'AbortError' || err.name === 'TimeoutError'
)
})
+38
View File
@@ -0,0 +1,38 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import getRunner from './runner.mjs'
test('OliveTinTestRunnerVm.start advances pageGeneration', async (t) => {
const originalRunner = process.env.OLIVETIN_TEST_RUNNER
const originalIp = process.env.IP
const originalPort = process.env.PORT
t.after(() => {
if (originalRunner === undefined) {
delete process.env.OLIVETIN_TEST_RUNNER
} else {
process.env.OLIVETIN_TEST_RUNNER = originalRunner
}
if (originalIp === undefined) {
delete process.env.IP
} else {
process.env.IP = originalIp
}
if (originalPort === undefined) {
delete process.env.PORT
} else {
process.env.PORT = originalPort
}
})
process.env.OLIVETIN_TEST_RUNNER = 'vm'
process.env.IP = '127.0.0.1'
process.env.PORT = '1337'
const runner = getRunner()
const before = runner.pageGeneration
await runner.start('pageGenerationFirst')
assert.equal(runner.pageGeneration, before + 1)
})
@@ -0,0 +1,34 @@
import { describe, it, after, afterEach } from 'mocha'
import { expect } from 'chai'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('runner: pageGeneration', function () {
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('reloads the dashboard after sequential config changes', async function () {
await runner.start('pageGenerationFirst')
await getRootAndWait()
let loadedDashboard = await webdriver.executeScript(
'return document.body.getAttribute("loaded-dashboard")'
)
expect(loadedDashboard).to.equal('Page Generation First')
await runner.start('pageGenerationSecond')
await getRootAndWait()
loadedDashboard = await webdriver.executeScript(
'return document.body.getAttribute("loaded-dashboard")'
)
expect(loadedDashboard).to.equal('Page Generation Second')
})
})
@@ -0,0 +1,13 @@
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: page-gen-first-action
shell: echo first
dashboards:
- title: Page Generation First
contents:
- title: page-gen-first-action
@@ -0,0 +1,13 @@
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: page-gen-second-action
shell: echo second
dashboards:
- title: Page Generation Second
contents:
- title: page-gen-second-action