const { test } = require('node:test'); const assert = require('node:assert'); const Module = require('module'); function withMocked(modulePath, mockExports, fn) { const resolved = require.resolve(modulePath); const original = require.cache[resolved]; require.cache[resolved] = new Module(resolved); require.cache[resolved].exports = mockExports; return Promise.resolve(fn()).finally(() => { if (original) { require.cache[resolved] = original; } else { delete require.cache[resolved]; } }); } test('buildCards skips composes without a valid PORTAL_APP_TYPE and reports fetch errors separately', async () => { delete require.cache[require.resolve('../src/dashboard')]; await withMocked('../src/dokployClient', { listComposes: async () => [ { composeId: 'c1', name: 'auth-redirect', composeStatus: 'done' }, { composeId: 'c2', name: 'gitea', composeStatus: 'done' }, { composeId: 'c3', name: 'broken-app', composeStatus: 'error' }, ], getComposeEnv: async (composeId) => { if (composeId === 'c1') return { env: 'PORTAL_APP_TYPE=web\nPORTAL_APP_URL=https://x', appName: 'auth-redirect-uzt5hx' }; if (composeId === 'c2') return { env: null, appName: 'gitea-xyz' }; throw new Error('boom'); }, getContainers: async () => [{ containerId: 'abc', state: 'running' }], }, async () => { const { buildCards } = require('../src/dashboard'); const cards = await buildCards('env-1'); assert.strictEqual(cards.length, 2); assert.strictEqual(cards[0].composeId, 'c1'); assert.strictEqual(cards[0].meta.appType, 'web'); assert.strictEqual(cards[0].containerState, 'running'); assert.deepStrictEqual(cards[1], { error: true, composeId: 'c3', name: 'broken-app' }); }); }); test('renderCard escapes HTML in labels and shows a "trigger" button for batch apps', () => { delete require.cache[require.resolve('../src/dashboard')]; const { renderCard } = require('../src/dashboard'); const html = renderCard({ composeId: 'c1', appName: 'sample', composeStatus: 'done', containerState: 'running', meta: { appType: 'batch', label: '', url: 'http://sample:3000', triggerPath: '/trigger' }, }); assert.ok(!html.includes('')); assert.ok(html.includes('<script>')); assert.ok(html.includes('data-action="trigger"')); assert.ok(!html.includes('data-action="start"')); }); test('renderCard shows an "状態取得失敗" message for error cards', () => { const { renderCard } = require('../src/dashboard'); const html = renderCard({ error: true, composeId: 'c3', name: 'broken-app' }); assert.ok(html.includes('状態取得失敗')); assert.ok(html.includes('broken-app')); });