Skip to content
Open
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
5 changes: 3 additions & 2 deletions public/js/setup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global Swal */
class SetupWizard {
constructor() {
this.bootstrap = window.__SETUP_BOOTSTRAP__ || {};
Expand Down Expand Up @@ -926,6 +927,7 @@ class SetupWizard {

applyPreset(preset) {
if (!preset) {
this.aiProvider.value = 'custom';
this.aiPresetHint.textContent = 'Manual mode: choose provider and enter values yourself. Token is optional for custom endpoints.';
return;
}
Expand Down Expand Up @@ -1665,7 +1667,7 @@ class SetupWizard {
try {
await navigator.clipboard.writeText(this.envPreview.value);
await this.showPopup({ icon: 'success', title: 'Copied', text: 'Environment keys copied to clipboard.' });
} catch (_error) {
} catch {
this.envPreview.select();
document.execCommand('copy');
await this.showPopup({ icon: 'success', title: 'Copied', text: 'Environment keys copied to clipboard.' });
Expand Down Expand Up @@ -1707,7 +1709,6 @@ class SetupWizard {
async finalizeSetup(options = {}) {
const validations = [];
for (let index = 0; index <= 5; index += 1) {
// eslint-disable-next-line no-await-in-loop
const valid = await this.validateStepBeforeContinue(index);
if (!valid) {
validations.push(index);
Expand Down
2 changes: 2 additions & 0 deletions scripts/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const TESTS = {
'quickstart-model-classification': 'test-quickstart-model-classification.js',
'quickstart-endpoint-protection': 'test-quickstart-endpoint-protection.js',
'setup-wizard-quickstart': 'test-setup-wizard-quickstart.js',
'setup-preset-manual-reset': 'test-setup-preset-manual-reset.js',
'rate-limiting': 'test-rate-limiting.js',
'scan-stop-flow': 'test-scan-stop-flow.js',
'setup-auth-endpoint-protection': 'test-setup-auth-endpoint-protection.js',
Expand Down Expand Up @@ -126,6 +127,7 @@ const AREAS = {
'quickstart-endpoint-protection',
'runtime-first-setup-state',
'setup-wizard-tag-default',
'setup-preset-manual-reset',
],
security: [
'setup-remote-guard',
Expand Down
214 changes: 214 additions & 0 deletions tests/test-setup-preset-manual-reset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');

const setupJsPath = path.join(__dirname, '..', 'public', 'js', 'setup.js');

function createMockClassList() {
const classes = new Set();
return {
add: (...items) => items.forEach((item) => classes.add(item)),
remove: (...items) => items.forEach((item) => classes.delete(item)),
toggle: (item, force) => {
if (force === undefined ? !classes.has(item) : force) {
classes.add(item);
return true;
}
classes.delete(item);
return false;
},
contains: (item) => classes.has(item)
};
}

function createMockElement(id) {
const listeners = {};
return {
id,
value: '',
checked: false,
disabled: false,
textContent: '',
innerHTML: '',
className: '',
style: {},
dataset: {},
classList: createMockClassList(),
appendChild: () => {},
addEventListener: (event, handler) => {
if (!listeners[event]) {
listeners[event] = [];
}
listeners[event].push(handler);
},
dispatchEvent: (eventName) => {
(listeners[eventName] || []).forEach((fn) => fn());
},
focus: () => {},
select: () => {},
setAttribute: () => {},
removeAttribute: () => {}
};
}

const elementIds = [
'setupWizardForm',
'setupProgressFill',
'setupStepLabel',
'adminUsername',
'adminPassword',
'confirmPassword',
'passwordHint',
'enableMfa',
'mfaSetupPanel',
'startMfaSetupBtn',
'mfaProvisioningBox',
'setupMfaQrImage',
'setupMfaSecret',
'setupMfaCode',
'confirmMfaCodeBtn',
'mfaStatusHint',
'paperlessUrl',
'paperlessUsername',
'paperlessToken',
'testPaperlessBtn',
'paperlessTestState',
'fetchMetadataBtn',
'metadataLoadState',
'documentsCount',
'correspondentsCount',
'tagsCount',
'scanAllDocuments',
'includeTag',
'excludeTagInput',
'addExcludeTagBtn',
'excludeTagsContainer',
'processedTag',
'excludeProcessedTagBtn',
'automaticScanEnabled',
'scanInterval',
'paperlessTagsDatalist',
'aiPreset',
'aiPresetHint',
'aiProvider',
'aiApiUrl',
'aiToken',
'aiModel',
'fetchAiModelsBtn',
'aiValidationTimeout',
'testAiBtn',
'aiTestState',
'aiModeQuickstartBtn',
'aiModeManualBtn',
'aiQuickstartPanel',
'aiManualPanel',
'quickstartBaseUrl',
'quickstartApiKey',
'quickstartDetectBtn',
'quickstartDetectState',
'quickstartHint',
'quickstartAiModel',
'quickstartOcrModel',
'quickstartEnableOcr',
'quickstartOcrHint',
'quickstartSaveRow',
'quickstartSaveBtn',
'ocrQuickstartNotice',
'mistralOcrEnabled',
'mistralFields',
'ocrProvider',
'ocrApiUrl',
'ocrApiUrlContainer',
'ocrApiKeyContainer',
'ocrApiKey',
'mistralOcrModel',
'fetchOcrModelsBtn',
'ocrValidationTimeout',
'testOcrBtn',
'ocrTestState',
'envPreview',
'copyEnvPreviewBtn',
'finalizeSetupBtn',
'prevStepBtn',
'nextStepBtn'
];

const elements = new Map(elementIds.map((id) => [id, createMockElement(id)]));

const steps = Array.from({ length: 7 }, (_unused, index) => ({
dataset: { stepTitle: `Step ${index + 1}` },
classList: createMockClassList(),
style: {},
disabled: false
}));

global.window = {
__SETUP_BOOTSTRAP__: { config: {}, defaults: {}, aiProviderPresets: [] },
fetch: async () => ({})
};

global.document = {
addEventListener: (_event, callback) => callback(),
querySelectorAll: (selector) => (selector === '.setup-step' ? steps : []),
querySelector: (selector) => {
if (selector === 'meta[name="csrf-token"]') {
return { getAttribute: () => '' };
}
return null;
},
getElementById: (id) => elements.get(id) || null,
createElement: (tagName) => createMockElement(tagName)
};

global.Swal = {
fire: async () => ({ isConfirmed: false }),
update: () => {},
close: () => {}
};

global.navigator = {
clipboard: {
writeText: async () => {}
}
};

global.Headers = class Headers {};
global.setInterval = () => 1;
global.clearInterval = () => {};

const source = fs.readFileSync(setupJsPath, 'utf8');
vm.runInThisContext(source, { filename: setupJsPath });

const wizard = window.setupWizard;
assert.ok(wizard, 'Setup wizard should initialize');

// Manual mode from a clean start already yields 'custom'
assert.strictEqual(wizard.aiProvider.value, 'custom', 'Fresh manual mode should default aiProvider to custom');

// Selecting a named preset (e.g. OpenAI) sets the hidden aiProvider field
const openAiPreset = {
id: 'openai',
label: 'OpenAI (ChatGPT)',
provider: 'openai',
apiUrl: 'https://api.openai.com/v1',
model: 'gpt-4o-mini',
tokenPlaceholder: 'sk-...'
};
wizard.applyPreset(openAiPreset);
assert.strictEqual(wizard.aiProvider.value, 'openai', 'Selecting the OpenAI preset should set aiProvider to openai');
assert.strictEqual(wizard.aiApiUrl.value, 'https://api.openai.com/v1');

// Switching back to "Manual custom configuration" (preset === null) must reset aiProvider
wizard.applyPreset(null);
assert.strictEqual(
wizard.aiProvider.value,
'custom',
'Switching back to manual configuration must reset the stale aiProvider value to custom'
);
assert.ok(
wizard.aiPresetHint.textContent.includes('Manual mode'),
'Manual mode hint should be shown after clearing the preset'
);

console.log('✅ test-setup-preset-manual-reset passed');
Loading