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'));
});