2026-07-12 13:00:02 +08:00
<!doctype html>
< html >
< head >
< meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > components.data.DataGrid tests< / title >
< script src = "../../ui.js" components = "data.DataGrid" > < / script >
< script >
2026-07-13 22:24:23 +08:00
const tableID = 'testTable'
const field = (id, name, type, settings) => ({ id, tableID, name, type, isIndex: false, memo: '', settings })
2026-07-12 13:00:02 +08:00
const gridFields = [
2026-07-13 22:24:23 +08:00
field('f-id', 'id', 'bi', { width: 70, type: 'number', pinned: 'left' }),
field('f-name', 'name', 'v100', { width: 160, type: 'text' }),
field('f-role', 'role', 'v30', { width: 120, type: 'select', options: [{ label: 'Administrator', value: 'admin' }, { label: 'Editor', value: 'editor' }] }),
field('f-score', 'score', 'ff', { width: 100, type: 'number', prefix: '$', decimals: 2 }),
field('f-active', 'active', 'b', { width: 90, type: 'switch' }),
field('f-choices', 'choices', 'v4096', { width: 130, type: 'checkbox', options: ['a', 'b'] }),
field('f-priority', 'priority', 'v100', { width: 120, type: 'radio', options: ['low', 'high'] }),
field('f-note', 'note', 't', { width: 160, type: 'textarea' }),
field('f-date', 'date', 'd', { width: 120, type: 'date' }),
field('f-datetime', 'datetime', 'dt', { width: 180, type: 'datetime' }),
field('f-tags', 'tags', 'v4096', { width: 140, type: 'TagsInput' }),
field('f-range', 'range', 'd', { width: 140, type: 'DatePicker' }),
field('f-color', 'color', 'v100', { width: 110, type: 'ColorPicker' }),
field('f-icon', 'icon', 'v100', { width: 120, type: 'IconPicker' })
2026-07-12 13:00:02 +08:00
]
const gridRows = Array.from({ length: 200 }, (_, index) => ({ id: index + 1, name: 'User ' + (index + 1), role: index % 2 ? 'editor' : 'admin', score: index + 0.5, active: index % 2 === 0, choices: ['a'], priority: 'high', note: 'Note ' + (index + 1), date: '2026-07-12', datetime: '2026-07-12T09:30', tags: ['grid'], range: '2026-07-12', color: '#0d6efd', icon: 'star' }))
< / script >
< / head >
< body class = "vh-100 overflow-hidden bg-body-tertiary" > < div class = "container-fluid h-100 py-3 d-flex flex-column" >
< header class = "d-flex align-items-center gap-2 mb-3 flex-shrink-0" > < h1 class = "h4 mb-0" > components.data.DataGrid< / h1 > < span id = "status" class = "badge text-bg-secondary" > 未测试< / span > < button id = "runAll" class = "btn btn-primary btn-sm ms-auto" type = "button" > 测试全部< / button > < span id = "coverage" class = "small text-body-secondary" > < / span > < / header >
< main class = "flex-fill overflow-hidden" > < DataGrid id = "usersGrid" class = "h-100" editable $ . state . fields = "gridFields" $ . state . list = "gridRows" > < / DataGrid > < / main > < pre id = "result" class = "h-25 overflow-auto border rounded bg-body p-2 mt-3 mb-0 flex-shrink-0 small" > < / pre >
< / div > < script >
2026-07-13 22:24:23 +08:00
const cases = ['editable', 'fields', 'list', 'async-binding', 'field', 'formType', 'format', 'select-editor', 'addRow', 'saveChanges', 'save-event', 'applySortFilter', 'sortConfig', 'filterConfig', 'virtual-scroll', 'selection-scroll', 'deleteField', 'api-mode', 'advanced-mode', 'DataGrid.registerFieldType', 'DataGrid.getFieldTypes']; const errors = []; const originalError = console.error; console.error = (...args) => { errors.push(args.map(String).join(' ')); originalError(...args) }; addEventListener('error', event => errors.push(String(event.message || event.error || event))); addEventListener('unhandledrejection', event => errors.push(String(event.reason || event))); const tick = (ms = 260) => new Promise(resolve => setTimeout(resolve, ms)); const assert = (r, feature, ok, message) => { r.assertions++; if (ok) r.passed++; else { r.failed++; r.failures.push({ feature, message }) } }
2026-07-12 13:00:02 +08:00
window.runTest = async () => { errors.length = 0; const r = { name: 'components.data.DataGrid', status: 'running', assertions: 0, passed: 0, failed: 0, failures: [], consoleErrors: [], coverage: { tested: [], total: cases, percent: 0 } }; await tick(); const grid = document.querySelector('#usersGrid');
assert(r, 'editable', grid.editable === true & & grid.querySelector('.dt-footer'), 'editable enables grid controls')
assert(r, 'fields', grid.state.fields.length === gridFields.length & & grid.querySelectorAll('.dt-header-row .dt-cell').length === gridFields.length, 'fields render grid columns')
assert(r, 'list', grid.state.list.length === 200 & & grid.querySelectorAll('.dt-body-row').length < 200 , ' list renders a virtual row window ' )
2026-07-13 22:24:23 +08:00
grid.state.list = undefined; await tick(); grid.state.list = gridRows; await tick(); assert(r, 'async-binding', grid.state.list.length === gridRows.length & & !errors.length, 'undefined async binding values do not break a later list update')
2026-07-12 13:00:02 +08:00
assert(r, 'field', grid.state.fields.every(field => field.id & & field.name), 'field definitions expose id and name')
2026-07-13 22:24:23 +08:00
assert(r, 'formType', ['text', 'number', 'select', 'switch', 'checkbox', 'radio', 'textarea', 'date', 'datetime', 'TagsInput', 'DatePicker', 'ColorPicker', 'IconPicker'].every(type => grid.state.fields.some(field => field.settings.type === type)), 'one grid visibly declares every supported editor type')
2026-07-12 13:00:02 +08:00
assert(r, 'format', grid.format('admin', grid.state.fields[2]) === 'Administrator' & & grid.format(1.5, grid.state.fields[3]) === '$1.50', 'field settings and formatters render values')
2026-07-13 22:24:23 +08:00
const roleCell = grid.querySelector('.dt-body-row .dt-cell[data-fidx="2"]'); grid.onMainDblClick({ target: roleCell }); await tick(); const roleSelect = grid.querySelector('.dt-editor-overlay select'); assert(r, 'select-editor', roleSelect?.value === 'admin' & & grid.state.fields[2].settings.type === 'select', 'settings.type selects the editor and preserves its initial value'); grid.hideEditor(true)
2026-07-12 13:00:02 +08:00
const beforeRows = grid.state.list.length; grid.addRow(); await tick(); assert(r, 'addRow', grid.state.list.length === beforeRows + 1 & & grid.state.isDirty, 'addRow adds a field-shaped row and marks changes dirty')
let save; grid.addEventListener('save', event => { save = event.detail }, { once: true }); grid.saveChanges(); assert(r, 'saveChanges', grid.state.isDirty === false, 'saveChanges clears pending changes'); assert(r, 'save-event', save?.list.length === beforeRows + 1 & & save?.fields.length === gridFields.length, 'save event exposes list and fields')
2026-07-13 22:24:23 +08:00
grid.state.activeFieldId = 'f-name'; grid.applySortFilter({ sort: 'desc' }); await tick(); assert(r, 'applySortFilter', grid.state.list[0].name === 'User 99', 'applySortFilter sorts the current rows')
assert(r, 'sortConfig', grid.state.sortConfig.fieldId === 'f-name' & & grid.state.sortConfig.direction === 'desc', 'sortConfig records field and direction')
grid.state.activeFieldId = 'f-role'; grid.state.filterConfig = { 'f-role': { mode: 'equals', value: 'admin', selectedValues: [] } }; grid.applySortFilter(); await tick(); assert(r, 'filterConfig', grid.state.list.every(row => row.role === 'admin'), 'filterConfig filters by field value')
2026-07-12 13:00:02 +08:00
const main = grid.querySelector('.dt-main'); main.scrollTop = main.scrollHeight; main.dispatchEvent(new Event('scroll')); await tick(); assert(r, 'virtual-scroll', grid.state._renderedList.length < grid.state.list.length & & grid . state . _listStartIndex > 0, 'virtual scroll updates the rendered row window')
const selected = grid.querySelector('.dt-body-row .dt-cell'); grid.onMainMouseDown({ target: selected, ctrlKey: false, metaKey: false }); main.scrollTop = 0; main.dispatchEvent(new Event('scroll')); await tick(400); main.scrollTop = main.scrollHeight; main.dispatchEvent(new Event('scroll')); await tick(400); assert(r, 'selection-scroll', grid.state.selectedRowCount === 1 & & grid.querySelectorAll('.dt-cell-selected').length > 0, 'selected cells restore their visible highlight after virtual scrolling away and back')
const fieldCount = grid.state.fields.length; grid.state.activeField = grid.state.fields.at(-1); const removing = grid.deleteField(); await tick(550); document.querySelector('Dialog')?.querySelectorAll('button')[1]?.click(); await removing; await tick(); assert(r, 'deleteField', grid.state.fields.length === fieldCount - 1 & & !grid.querySelector('.null') & & !grid.textContent.includes('null'), 'deleteField removes its cells without orphan null rows')
2026-07-13 22:24:23 +08:00
const request = HTTP.request; const requests = []; HTTP.request = async opt => { requests.push(opt.data); if (opt.data.action === 'fields') return { ok: true, result: { ok: true, fields: [field('remote-role', 'Role', 'v30', { type: 'select', options: ['Admin', 'User'] })] } }; if (opt.data.action === 'query') return { ok: true, result: { ok: true, data: [{ id: 'remote-1', Role: 'Admin', creator: '_system' }] } }; return { ok: true, result: { ok: true } } }; grid.apiUrl = '/admin/table'; grid.state.table = 'remote-users'; await tick(); assert(r, 'api-mode', grid.state.fields.length === 1 & & grid.state.list[0]?.Role === 'Admin' & & requests.some(req => req.action === 'fields') & & requests.some(req => req.action === 'query' & & Array.isArray(req.filter) & & Array.isArray(req.sort)), 'api mode loads fields and rows through safe filter and sort POST actions'); grid.state.advancedMode = true; await tick(); assert(r, 'advanced-mode', grid.state._displayFields.length === 6 & & grid.state._displayFields.at(-1).name === 'updateTime', 'advanced mode adds internal read-only system columns without changing fields'); HTTP.request = request
2026-07-12 13:00:02 +08:00
DataGrid.registerFieldType({ value: 'test', label: 'Test', schema: [] }); assert(r, 'DataGrid.registerFieldType', DataGrid.getFieldTypes().some(type => type.value === 'test'), 'registerFieldType registers a public field type'); assert(r, 'DataGrid.getFieldTypes', DataGrid.getFieldTypes().length >= 8, 'getFieldTypes returns built-in and registered types')
r.coverage.tested = [...cases]; r.coverage.percent = 100; r.consoleErrors = [...errors]; r.status = r.failed || r.consoleErrors.length ? 'failed' : 'passed'; document.querySelector('#status').className = 'badge text-bg-' + (r.status === 'passed' ? 'success' : 'danger'); document.querySelector('#status').textContent = r.status === 'passed' ? '通过' : '失败'; document.querySelector('#coverage').textContent = '覆盖 100%'; document.querySelector('#result').textContent = JSON.stringify(r, null, 2); return r }
document.querySelector('#runAll').onclick = window.runTest
< / script > < / body > < / html >