Prechádzať zdrojové kódy

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>
jamesread 19 hodín pred
rodič
commit
a42b640331

+ 1 - 1
frontend/package.json

@@ -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
frontend/resources/vue/App.vue

@@ -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
frontend/resources/vue/utils/themeLoader.js

@@ -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}`)
   }

+ 57 - 0
frontend/resources/vue/utils/themeLoader.test.mjs

@@ -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 - 0
integration-tests/runner.test.mjs

@@ -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)
+})

+ 34 - 0
integration-tests/tests/pageGeneration/pageGeneration.mjs

@@ -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')
+  })
+})

+ 13 - 0
integration-tests/tests/pageGenerationFirst/config.yaml

@@ -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

+ 13 - 0
integration-tests/tests/pageGenerationSecond/config.yaml

@@ -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