Spaces:
Running
Running
File size: 6,184 Bytes
96bdf6c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import type { ApiSettings } from './api-settings-dialog';
import { I18nProvider } from '@/lib/i18n';
import { renderInClientDom } from '@/test-utils/react-dom';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
const emptySettings: ApiSettings = { apiKey: '', baseUrl: '' };
const savedSettings: ApiSettings = {
apiKey: 'saved-key',
baseUrl: 'https://example.test/v1'
};
const replacementSettings: ApiSettings = {
apiKey: 'replacement-key',
baseUrl: 'https://replacement.example/v1'
};
type ApiSettingsDialogComponent = typeof import('./api-settings-dialog').ApiSettingsDialog;
let ApiSettingsDialog: ApiSettingsDialogComponent;
let sharedView: Awaited<ReturnType<typeof renderInClientDom>>;
let ownerDocument: Document;
async function waitForActiveElement(target: HTMLElement, timeoutMs = 1000) {
const deadline = Date.now() + timeoutMs;
while (target.ownerDocument.activeElement !== target) {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) return false;
await new Promise((resolve) => setTimeout(resolve, Math.min(10, remainingMs)));
}
return true;
}
before(async () => {
sharedView = await renderInClientDom(<div />);
ownerDocument = sharedView.container.ownerDocument;
({ ApiSettingsDialog } = await import('./api-settings-dialog'));
});
after(async () => {
await sharedView?.cleanup();
});
function renderDialog(
isOpen: boolean,
settings: ApiSettings,
options: {
onOpenChange?: (open: boolean) => void;
onSave?: (nextSettings: ApiSettings) => void;
getReturnFocusTarget?: () => HTMLButtonElement | null;
} = {}
) {
return (
<I18nProvider>
<ApiSettingsDialog
isOpen={isOpen}
onOpenChange={options.onOpenChange ?? (() => {})}
settings={settings}
onSave={options.onSave ?? (() => {})}
getReturnFocusTarget={options.getReturnFocusTarget ?? (() => null)}
/>
</I18nProvider>
);
}
describe('ApiSettingsDialog draft state', { concurrency: false }, () => {
it('syncs restored settings, saves the current draft, and clears settings with stable dialog lifecycle', async () => {
const savedValues: ApiSettings[] = [];
const openChanges: boolean[] = [];
await sharedView.render(renderDialog(false, emptySettings));
await sharedView.render(renderDialog(false, savedSettings));
await sharedView.render(renderDialog(true, savedSettings));
let apiKeyInput = ownerDocument.querySelector<HTMLInputElement>('#api-key-input');
let baseUrlInput = ownerDocument.querySelector<HTMLInputElement>('#api-base-url-input');
assert.ok(apiKeyInput, 'missing API key input');
assert.ok(baseUrlInput, 'missing API URL input');
assert.equal(apiKeyInput.value, savedSettings.apiKey);
assert.equal(baseUrlInput.value, savedSettings.baseUrl);
await sharedView.render(renderDialog(true, replacementSettings));
apiKeyInput = ownerDocument.querySelector<HTMLInputElement>('#api-key-input');
assert.ok(apiKeyInput, 'missing API key input after external settings update');
assert.equal(apiKeyInput.value, savedSettings.apiKey);
await sharedView.render(renderDialog(false, replacementSettings));
await sharedView.render(renderDialog(true, replacementSettings));
apiKeyInput = ownerDocument.querySelector<HTMLInputElement>('#api-key-input');
baseUrlInput = ownerDocument.querySelector<HTMLInputElement>('#api-base-url-input');
assert.ok(apiKeyInput, 'missing reopened API key input');
assert.ok(baseUrlInput, 'missing reopened API URL input');
assert.equal(apiKeyInput.value, replacementSettings.apiKey);
assert.equal(baseUrlInput.value, replacementSettings.baseUrl);
await sharedView.render(renderDialog(false, savedSettings));
await sharedView.render(
renderDialog(true, savedSettings, {
onOpenChange: (open) => openChanges.push(open),
onSave: (settings) => savedValues.push(settings)
})
);
const currentApiKeyInput = ownerDocument.querySelector<HTMLInputElement>('#api-key-input');
const saveButton = [...ownerDocument.querySelectorAll<HTMLButtonElement>('button')].find(
(button) => button.textContent === '保存'
);
assert.ok(currentApiKeyInput, 'missing API key input');
assert.ok(saveButton, 'missing save button');
await sharedView.click(saveButton);
assert.deepEqual(savedValues, [savedSettings]);
await sharedView.render(
renderDialog(false, savedSettings, {
onOpenChange: (open) => openChanges.push(open),
onSave: (settings) => savedValues.push(settings)
})
);
await new Promise((resolve) => setTimeout(resolve, 500));
assert.deepEqual(openChanges, []);
const focusTarget = ownerDocument.createElement('button');
ownerDocument.body.append(focusTarget);
await sharedView.render(
renderDialog(true, savedSettings, {
onSave: (settings) => savedValues.push(settings),
getReturnFocusTarget: () => focusTarget
})
);
const clearButton = [...ownerDocument.querySelectorAll<HTMLButtonElement>('button')].find(
(button) => button.textContent === '清空'
);
assert.ok(clearButton, 'missing clear button');
await sharedView.click(clearButton);
assert.deepEqual(savedValues, [savedSettings, emptySettings]);
await sharedView.render(
renderDialog(false, savedSettings, {
onSave: (settings) => savedValues.push(settings),
getReturnFocusTarget: () => focusTarget
})
);
const focusWasRestored = await waitForActiveElement(focusTarget);
focusTarget.remove();
assert.equal(focusWasRestored, true, 'dialog focus was not restored');
});
});
|