/**
 * Layout information for a single cell
 */
export interface CellLayout {
    /** Cell left-edge X coordinate (relative to canvas) */
    x: number;
    /** Cell top-edge Y coordinate (relative to canvas) */
    y: number;
    /** Cell width in pixels */
    width: number;
    /** Cell height in pixels */
    height: number;
    /** Whether the cell is within the visible viewport */
    visible: boolean;
    /** Column key */
    columnKey: string;
    /** Row ID (number for normal/viewer mode, string for grouped mode) */
    rowId: string | number;
    /** Column index */
    colIndex: number;
    /** Row index */
    rowIndex: number;
    /** Whether this is a fixed column ('left' | 'right' | '') */
    fixed: string;
}
/**
 * A visible cell in the viewport with its layout info
 */
export interface VisibleCell extends CellLayout {
    /** Cell display value */
    value: any;
    /** Cell display label (for select-type columns, etc.) */
    label: string;
}
/**
 * Column configuration exposed to tests
 */
export interface TestColumnConfig {
    /** Column unique key */
    key: string;
    /** Column display title */
    title: string;
    /** Column width in pixels */
    width: number;
    /** Whether this column is fixed ('left' | 'right' | '') */
    fixed: string;
    /** Whether this column is visible on screen */
    visible: boolean;
    /** Column index in the header */
    index: number;
    /** Column level (0 = top-level, 1 = nested child) */
    level: number;
    /** Column type */
    type: string;
    /** Nested child columns (if this is a parent column with nested sub-columns) */
    children: TestColumnConfig[] | null;
    /** Number of columns this header spans */
    colspan: number;
    /** Number of rows this header spans */
    rowspan: number;
    /** Parent column key (for nested child columns) */
    parentKey: string | null;
}
/**
 * Current scroll position of the table
 */
export interface ScrollPosition {
    /** Horizontal scroll offset (negative value, 0 = no scroll) */
    scrollX: number;
    /** Vertical scroll offset (negative value, 0 = no scroll) */
    scrollY: number;
    /** Maximum horizontal scroll distance */
    maxScrollX: number;
    /** Maximum vertical scroll distance */
    maxScrollY: number;
    /** Whether a horizontal scrollbar is present */
    hasHorizontalScrollbar: boolean;
    /** Whether a vertical scrollbar is present */
    hasVerticalScrollbar: boolean;
}
/**
 * Overall table dimension information
 */
export interface TableDimensions {
    /** Container (canvas) width in pixels */
    containerWidth: number;
    /** Container (canvas) height in pixels */
    containerHeight: number;
    /** Total table content width (sum of all column widths) */
    tableWidth: number;
    /** Total table content height (sum of all row heights) */
    tableHeight: number;
    /** Header area height in pixels */
    headerHeight: number;
    /** Default row height in pixels */
    rowHeight: number;
    /** Fixed left columns total width */
    fixedLeftWidth: number;
    /** Fixed right columns total width */
    fixedRightWidth: number;
    /** Total number of data rows */
    totalRows: number;
    /** Total number of leaf columns */
    totalColumns: number;
}
/**
 * A single difference found during snapshot comparison
 */
export interface SnapshotDifference {
    /** Difference category */
    type: 'cellLayout' | 'columnConfig' | 'scroll' | 'dimensions';
    /** JSON path to the differing value (e.g. "cells[0].x") */
    path: string;
    /** Expected value from the saved snapshot */
    expected: any;
    /** Actual current value */
    actual: any;
}
/**
 * Result of comparing the current state against a saved snapshot
 */
export interface SnapshotCompareResult {
    /** Whether the current state matches the snapshot (within tolerance) */
    isEqual: boolean;
    /** List of differences found */
    differences: SnapshotDifference[];
}
/**
 * A complete table state snapshot
 */
export interface TableSnapshot {
    /** Snapshot identifier */
    name: string;
    /** Timestamp when the snapshot was created */
    timestamp: number;
    /** All visible cells at the time of snapshot */
    visibleCells: VisibleCell[];
    /** All column configurations */
    columns: TestColumnConfig[];
    /** Scroll position */
    scrollPosition: ScrollPosition;
    /** Table dimensions */
    dimensions: TableDimensions;
}
/**
 * Per-column header vs body width comparison result
 */
export interface HeaderBodyWidthComparison {
    colIndex: number;
    columnKey: string;
    headerWidth: number;
    bodyCellWidth: number;
    isNested: boolean;
    match: boolean;
}
/**
 * Header column info exposed to tests for style validation
 */
export interface HeaderColumnInfo {
    /** Unique column identifier */
    colId: string;
    /** Column index */
    index: number;
    /** Display text */
    text: string;
    /** Column type (nested, nested-child, etc.) */
    colType: string;
    /** Column span */
    colspan: number;
    /** Whether this column is associated with TAPD */
    isTapdRelated: boolean;
    /** Whether this column is associated with IOMC */
    isIomc: boolean;
    /** X position on screen */
    x: number;
    /** Y position */
    y: number;
    /** Width */
    width: number;
    /** Height */
    height: number;
}
/**
 * Main Test Hooks API exposed on window.__CANVAS_TABLE_TEST_HOOKS__
 *
 * All methods return deep copies of internal state to prevent
 * accidental mutation from test code.
 */
export interface TestHooksAPI {
    /**
     * Get layout information for a specific cell.
     * @param rowId - Row identifier (number for normal/viewer, string for grouped)
     * @param columnKey - Column key string
     * @returns CellLayout or null if the cell does not exist
     */
    getCellLayout: (rowId: string | number, columnKey: string) => CellLayout | null;
    /**
     * Get all cells currently visible in the viewport.
     * Only returns cells that are rendered by the virtual scroll engine.
     * @returns Array of VisibleCell objects
     */
    getAllVisibleCells: () => VisibleCell[];
    /**
     * Get configuration for a specific column.
     * Supports both top-level and nested child columns.
     * @param columnKey - Column key string
     * @returns TestColumnConfig or null if column not found
     */
    getColumnConfig: (columnKey: string) => TestColumnConfig | null;
    /**
     * Get data for a specific row.
     * @param rowId - Row identifier
     * @returns Row data object or null if not found
     */
    getRowData: (rowId: string | number) => Record<string, any> | null;
    /**
     * 获取组头行搜索命中高亮状态。
     *
     * 返回值说明：
     * - `matched`：组头行是否被搜索命中（searchMatchList 中存在 isGroupHeader: true 且 id 匹配的条目）
     * - `currentHighlight`：是否是"当前高亮"项（命中条目中存在 highlight: true 的 field），为 true 时 GroupRow 会绘制 2px 橙色 inset 边框
     *
     * @param rowId - 组头行 id（分组模式下通常是 string）
     * @returns 命中状态或 null（行不存在 / 非组头行）
     */
    getGroupHeaderSearchMatchState: (rowId: string | number) => {
        matched: boolean;
        currentHighlight: boolean;
    } | null;
    /**
     * 获取组头行搜索命中高亮的"首列左边框" x 坐标（canvas 相对坐标）。
     *
     * 用于验证多层分组（1D/2D/3D）下左边框贴合分组名称块左缘的正确性。
     *
     * @param rowId - 组头行 id
     * @returns x 坐标或 null（行不存在 / 非组头行）
     */
    getSearchHighlightFirstLeftBorderX: (rowId: string | number) => number | null;
    /**
     * Get the current scroll position.
     * @returns ScrollPosition object
     */
    getScrollPosition: () => ScrollPosition;
    /**
     * Get overall table dimensions.
     * @returns TableDimensions object
     */
    getTableDimensions: () => TableDimensions;
    /**
     * Create a snapshot of the current table state.
     * @param name - Snapshot identifier
     * @returns The created TableSnapshot
     */
    createSnapshot: (name: string) => TableSnapshot;
    /**
     * Compare the current table state against a previously saved snapshot.
     * @param name - Name of the snapshot to compare against
     * @param tolerance - Pixel tolerance for coordinate comparisons (default: 1)
     * @returns SnapshotCompareResult with differences
     */
    compareSnapshot: (name: string, tolerance?: number) => SnapshotCompareResult;
    /**
     * Get a previously saved snapshot by name.
     * @param name - Snapshot identifier
     * @returns TableSnapshot or null if not found
     */
    getSnapshot: (name: string) => TableSnapshot | null;
    /**
     * Export a snapshot as a JSON string (for saving to file).
     * @param name - Snapshot identifier
     * @returns JSON string or null if snapshot not found
     */
    exportSnapshot: (name: string) => string | null;
    /**
     * Import a snapshot from a JSON string.
     * @param json - JSON string of a TableSnapshot
     */
    importSnapshot: (json: string) => void;
    /**
     * Compare header column widths with body cell widths.
     * Detects header/body width mismatches, especially for nested columns.
     * @returns Per-column comparison results
     */
    getHeaderBodyWidthComparison: () => HeaderBodyWidthComparison[];
    /**
     * Get header column info including business properties and hover state.
     * Returns all column headers with their style/icon properties.
     */
    getHeaderInfo: () => HeaderColumnInfo[];
    /**
     * Get the currently hovered header column ID.
     */
    getHoveredHeaderColId: () => string | null;
    /**
     * Get all nested grids currently visible in the viewport.
     * Returns position, size, selection state, and cell info for each nested grid.
     */
    getNestedGridInfoList: () => NestedGridTestInfo[];
    /**
     * Get nested grid info for a specific parent cell.
     * @param parentRowId - Parent row identifier
     * @param parentColKey - Parent column key containing the nested grid
     */
    getNestedGridInfo: (parentRowId: string | number, parentColKey: string) => NestedGridTestInfo | null;
    /**
     * Get the canvas-relative bounding box of a nested cell.
     * Useful for dispatching mouse events on a specific nested cell.
     * @param parentRowId - Parent row identifier
     * @param parentColKey - Parent column key
     * @param nestedRow - Nested grid row index
     * @param nestedCol - Nested grid column index
     */
    getNestedCellBounds: (parentRowId: string | number, parentColKey: string, nestedRow: number, nestedCol: number) => {
        x: number;
        y: number;
        width: number;
        height: number;
        canvasX: number;
        canvasY: number;
    } | null;
    /**
     * Get all interactive (clickable) areas within a specific cell.
     * Returns canvas-relative coordinates for edit icon, link icon, error icon, etc.
     * Used by Playwright to simulate clicks on specific cell regions.
     */
    getCellInteractiveAreas: (rowId: string | number, columnKey: string) => CellInteractiveAreas | null;
    /**
     * Get interactive areas within a column header.
     * Returns sort/filter icon, resize handle, and settings icon bounds.
     */
    getHeaderInteractiveAreas: (colIndex: number) => HeaderInteractiveAreas | null;
    /**
     * Get interactive areas within a group row header.
     * Returns expand arrow and checkbox bounds.
     */
    getGroupRowAreas: (rowId: string) => GroupRowAreas | null;
    /**
     * 获取普通数据行 checkbox 的精确 canvas 坐标（与 RowHeader 的命中检测严格对齐）。
     *
     * LRN-20260512-001 约定的"长期稳定 e2e 锚点 API"：批量下载 / 多选 spec 必须能
     * 在 canvas 模式下点中行 checkbox 而不是行号。原本 e2e 端按"index 列宽度的一半"
     * 估算坐标，在分组 / 不同 originFixedWidth 下会偏到行号区，触发 onRowClick
     * 而不是 checkbox toggle。
     *
     * @param rowId - 行标识
     * @returns 16×16 px 的 checkbox 区域（canvas 相对坐标），关闭索引列 / checkbox /
     *          组头行 / 行不存在时返回 null。
     */
    getRowCheckboxArea: (rowId: string | number) => RowCheckboxArea | null;
    /**
     * Get all visible group rows with their expand/collapse state.
     */
    getVisibleGroupRows: () => VisibleGroupRowInfo[];
    /**
     * 列出当前可见的所有 ⊕ AddTaskRow 行的 canvas 几何（task=be29ce06）。
     *
     * 仅返回当前 `body.rows` 中 `instanceof AddTaskRow` 的行——即虚拟滚动已经
     * 实例化的 AddTaskRow（hit-test 必须基于已实例化的行：`isInsideIconArea`
     * 依赖 `row.y` / `row.height` / `data._parentGroupLevel`）。
     *
     * 调用前 spec 应先确保表格已滚动到目标 ⊕ 行所在视口；本 hook 不做隐式 scroll
     * （spec 用 `scrollToRow` 或 wheel 自己定位）。
     *
     * 这是 LRN-20260512-001 约定的"长期稳定 e2e 锚点 API"——业务侧 / 表格侧
     * 重构时不要随手隐藏。详见 CONTROLLER_API_CONTRACT.md `getAddTaskRowAreas`
     * 段。
     */
    getAddTaskRowAreas: () => AddTaskRowArea[];
    /**
     * Get current selection, editing, and checked-row state.
     */
    getSelectionState: () => SelectionState;
    /**
     * 仅返回当前已勾选行（checkbox-selected）的 rowId 列表。
     *
     * 与 `getSelectionState().checkedRowIds` 等价，但作为独立的稳定 hook 暴露，
     * 给 E2E 单点查询用，避免上层为了"行勾选状态"再读完整 SelectionState。
     *
     * 这是按 LRN-20260512-001 约定的"长期稳定 e2e 锚点 API"——业务侧 / 表格侧
     * 重构时不要随手隐藏。
     */
    getCheckedRowIds: () => (string | number)[];
    /**
     * 按"任务名 / 显示名"反查 row 的 user-facing rowKey 值（一般是 task id）。
     *
     * LRN-20260512-001 + LRN-20260512-005 衍生需求：之前 AmE2ETest 端
     * `findRowIdByTaskName` 自己分三层 evaluate（visibleCells → grid.body.data
     * → controller.tableTaskList），每一层都做一次完整深拷贝，刚 setExtraTableData
     * 后还存在 hooks 短暂返回 null 的窗口。表格本体直接给一个稳定 entry，
     * 走 `body.data` 全量扫描（不依赖 rowCache 是否实例化），开销小且与
     * `getCheckedRowIds` 的 id 类型严格对齐。
     *
     * 匹配规则：
     * - 默认候选字段：`name`, `task_name`, `taskName`, `title`
     *   （可经 `options.fields` 自定义）
     * - 字段值是对象时（自定义 viewer，如交付物列返回 `{ files: [...] }`），
     *   展开 `value.name / .task_name / .taskName / .title / .value / .label / .text`
     *   再匹配字符串。
     * - 优先精确相等，其次 `includes` 子串匹配（设 `options.exact = true` 关闭子串）。
     * - 跳过组头 / 分隔行 / 加任务行（这些 row 的 rowKey 通常不是真实 id）。
     *
     * @returns 找到时返回 `row[rowKey]`（保留 number/string 原始类型）；未找到返回 null。
     */
    findRowIdByName: (name: string, options?: {
        fields?: string[];
        exact?: boolean;
    }) => string | number | null;
    /**
     * 按列显示名（header text / title）反查列的业务 `key`（即 `column.key`）。
     *
     * LRN-20260512-002：column.key 是业务字段名（如 `'deliverable_v2'`），
     * 而 `getHeaderInfo()[i].colId` 是数字字符串——很多 e2e helper 把两者混用，
     * `getCellLayout` / `getCellInteractiveAreas` 入参拿到 colId 永远 null。
     *
     * 这里把"displayName → column.key"映射收敛进表本体，e2e 端不再需要先打到
     * 宿主 controller。
     *
     * @param displayName - 列的显示名（如 "交付物"）
     * @param options.exact - 默认 false：includes 匹配；true 时仅做精确相等
     * @returns column.key 或 null
     */
    getColumnKeyByName: (displayName: string, options?: {
        exact?: boolean;
    }) => string | null;
    /**
     * 程序化滚动让指定 rowId 的行进入可见视口。
     *
     * 仅做"垂直方向"滚动，水平滚动位置保持不变（避免破坏 e2e 中已对齐到目标列的滚动状态）。
     * 行不存在 / rowId 找不到 → 返回 false；已可见或滚动到位 → 返回 true。
     *
     * 用于替代 e2e helper 里 N 次 wheel-scroll 的循环——直接 jump 到目标行，
     * 避开"刚上传/刚刷新"导致 visibleCells 短时间没有目标行的窗口。
     */
    scrollToRow: (rowId: string | number) => boolean;
    /**
     * 把指定 key 的列横向滚动到非冻结视口内（task=89c1ec62）。
     *
     * 仅做"水平方向"滚动；视口左/右边界扣除冻结列宽（fixedLeftWidth /
     * width - fixedRightWidth）。已完全可见 / 冻结列 / key 不存在 → no-op。
     * 内部走 DataGrid.scrollColumnIntoViewByKey（其再调 scrollTo，会 clamp 到
     * [maxScrollX, 0]），不抛错。
     *
     * 给 AssetMatrix 「设置显示字段」popper 点字段名时把列漏出来用，也给
     * AmE2ETest 在 spec 里精确断言（先 scrollColumnIntoViewByKey 再
     * isColumnFullyVisibleByKey 验证）用。
     */
    scrollColumnIntoViewByKey: (key: string) => void;
    /**
     * 判断指定 key 的列是否完全位于「非冻结视口」内（task=89c1ec62）。
     *
     * - 冻结列（fixed === 'left' | 'right'）恒返回 true（永远可见）
     * - 列不存在 → 返回 false
     * - 否则按 viewLeft = fixedLeftWidth / viewRight = width - fixedRightWidth
     *   判定 screenLeft / screenRight 是否完全位于视口内
     */
    isColumnFullyVisibleByKey: (key: string) => boolean;
    /**
     * 一次性把"任务名 + 列显示名 → cell interactive areas"全套查清楚的复合 hook。
     *
     * task=3f7204c0/iter#2 加固：把 spec 端的多步 evaluate 压成单次浏览器 tick，
     * 避免 batch-download / deliverable-upload-download 等 spec 在每两步之间被
     * setExtraTableData 触发的 row-reorder / re-layout 打断（旧路径需要 ① findRowIdByName
     * → ② getColumnKeyByName → ③ scrollToRow → ④ getCellInteractiveAreas，每一步都
     * 跨一个 tick）。
     *
     * 内部行为：
     *   1) `findRowIdByName(taskName)` 拿 rowId（含 body.data → _fullData fallback）
     *   2) `getColumnKeyByName(columnDisplayName)` 拿 column.key
     *   3) 若 `scrollIfNeeded !== false`，自动调 `scrollRowIntoViewById` 让行进入视口
     *   4) `getCellInteractiveAreas(rowId, columnKey)` 取交互区域（cell / editIcon /
     *      linkIcon / errorIcon / reminderTriangle / viewerAreas / meta）
     *
     * 失败时返回 `{ ok: false, reason }`，reason 详细分类：
     *   - `'row-not-found'`     : taskName 在 body.data 与 _fullData 都查不到
     *   - `'column-not-found'`  : 列显示名找不到对应 column.key
     *   - `'row-not-visible'`   : rowId 在 _fullData 里有但被分组折叠隐藏（spec 应先展开分组）
     *   - `'cell-not-found'`    : 极端情况（cell 在 row.allCells 里缺失，通常是 rowCache 异步未就绪）
     *
     * 成功时返回 `{ ok: true, rowId, columnKey, areas }`，areas 字段格式与
     * `getCellInteractiveAreas` 完全一致。
     */
    /**
     * 诊断 hook：返回 grid 内部状态摘要。
     *
     * task=cdc1b0da/iter#4 加固：spec 在 canvas 模式下连续 3 轮出现 `findRowIdByName`
     * 0/10 命中——既有 hook 全部"找不到就返回 null"，观察侧拿不到中间状态。本 hook 给
     * spec / verify 一个"实地探查"入口：可以快速判断 grid 是不是空、rowKey 是不是预期、
     * 任务名是否真的存在但被字段白名单筛掉、前 3 行字段集合是什么样。
     *
     * 返回结构刻意保持小（前 3 行 sample, 字段名列表）—— 整段 console.log 也不会爆。
     *
     * @param name - 可选；提供时做一次 case-insensitive includes 扫描，返回命中数 + 首批样本
     */
    getDiagnostics: (name?: string) => {
        rowKey: string;
        bodyDataLength: number;
        fullDataLength: number;
        columnsCount: number;
        sampleRowFieldNames: string[];
        sampleRows: Array<Record<string, any>>;
        nameProbe: {
            query: string;
            bodyMatchCount: number;
            fullMatchCount: number;
            bodyHits: Array<{
                rowIndex: number;
                matchedField: string;
                matchedValue: string;
                rowKeyValue: any;
            }>;
        } | null;
        /**
         * task=b1464bc3/iter#4：freeze-column 诊断专用——top-level columns 的 fixed
         * 状态摘要。spec 在 canvas 模式调 `__AM_E2E__.controller.freezeColumns(...)`
         * 后会轮询 `grid.columns[i].fixed`，本字段让 spec / verify 在轮询超时时
         * 一次性 dump "key/name/fixed/hasChildren"，精准区分"controller 没改"
         * vs "grid 还没把 fixed 广播出去"两种根因。最多 50 条，避免大表 console 爆掉。
         */
        columnsSummary: Array<{
            index: number;
            key: string;
            name: string;
            fixed: string;
            hasChildren: boolean;
        }>;
    };
    /**
     * task=f479647b/iter#1：freeze-column 长期卡死专用诊断探针。
     *
     * 背景：spec `freeze-column.spec.ts` 在 canvas 分支调
     * `freezeFieldsViaController(page, [target])`，candidate 列表里依次尝试
     * `freezeColumns / setFreezeColumns / freezeColumn / toggleFreezeColumn`，
     * 全部 fail 后吐 `controller.toggleFreezeColumn is not a function`——历史 4 轮
     * 都凭这条错误信息推断"AssetMatrix 没暴露 API"，但实际业务侧（FillerInitController）
     * 已经 public 暴露 freezeColumn(s) / unfreezeColumn(s) / toggleFreezeColumn / setFreezeColumns
     * 等多个方法。所以问题不在"业务有没有实现"，而是 spec 端拿到的
     * `__AM_E2E__.controller` **运行时形状**与预期不符（candidate name 全找不到）。
     *
     * 这个探针不动业务逻辑，仅做"实地探查"——把 spec 失败现场需要看到的事实
     * 一次性 dump 出来：
     *
     *   - `available`: window.__AM_E2E__.controller 是否真的挂上了
     *   - `constructorName`: ctrl.constructor.name（运行时类名，区分 mainPanel
     *      controller vs 子表 dialog controller vs perspectiveTable）
     *   - `propsName`: ctrl.props?.name（确认是不是 "mainPanel"）
     *   - `prototypeChain`: 原型链每一层的 constructor.name（ts-mixer 拼出来的
     *      Mixin 链，让我们能看到 FillerInitController / FillerDataGridController
     *      / Disposable 等具体到哪层挂的方法）
     *   - `freezeMethodsByLevel`: 原型链每一层 own keys 中匹配 /freeze/i 的并集
     *      （包含父类被 ts-mixer 拷贝过来的方法名 vs 真实定义所在的层）
     *   - `typeofChecks`: spec 实际调的 candidate name 对应的 typeof 结果，让我们
     *      一眼看到"freezeColumns 是 function 吗""toggleFreezeColumn 是 undefined 吗"
     *   - `byNameKeys`: __AM_E2E__.byName 里都有哪些子表实例（用于排查
     *      "spec 拿到的 controller 是不是任务池主表，而不是 dialog 内的子表"）
     *
     * **关键设计**：这个 hook 失败时永不抛错——任何 try/catch 都吞掉；返回结构
     * 字段可能为空但 shape 始终不变（spec 端可放心 JSON.stringify）。
     *
     * spec 调用建议：
     * ```ts
     * const probe = await page.evaluate(() => {
     *   return (window as any).__CANVAS_TABLE_TEST_HOOKS__?.probeAmController?.();
     * });
     * console.log('[freeze-column/probe]', JSON.stringify(probe));
     * ```
     * （prefix 必须 `[freeze-column/probe]`，orchestrator 摘 observation 时按此匹配。）
     *
     * @returns 诊断对象；__AM_E2E__ 未挂时 available=false，其他字段为合理空值
     */
    probeAmController: () => {
        available: boolean;
        reason: string;
        constructorName: string;
        propsName: string;
        tableType: string;
        isMainPanel: boolean;
        prototypeChain: string[];
        freezeMethodsByLevel: Array<{
            level: number;
            className: string;
            methods: string[];
        }>;
        freezeMethodsAll: string[];
        typeofChecks: Record<string, string>;
        byNameKeys: string[];
        hasDataGrid: boolean;
        hasTable: boolean;
        /**
         * task=f479647b/iter#3 新增：install 时间戳与计数器，给 spec 端算
         * "spec 调用时机相对 hooks 安装时机"的偏移用。
         *
         * 字段含义：
         *   - hooksInstalledAt      : Date.now() at first installTestHooks() call.
         *                              (任何一个 DataGrid 实例第一次 mount 时被钉住)
         *   - hooksLastInstalledAt  : Date.now() at most recent installTestHooks().
         *                              (HMR / 多 DataGrid 时持续刷新)
         *   - hooksInstallCount     : 累计 install 次数。
         *   - nowAtProbe            : Date.now() at the moment probe is called.
         *                              (spec 端算 nowAtProbe - hooksInstalledAt 即拿到偏移)
         *   - hasHooksAlias         : window.__CANVAS_TABLE_TEST_HOOKS__ 是否真的指向某个 hooks。
         *                              （正常 ≥ 1 install 之后必为 true）
         *   - hasGridAlias          : window.__CANVAS_TABLE_GRID__ 是否真的指向某 DataGrid。
         *                              （正常 ≥ 1 install 之后必为 true）
         *
         * 0 / false 表示未安装或浏览器无 Date.now（非常规环境）；spec 应把
         * `hooksInstalledAt === 0` 当成"hooks 还没装"信号，不要 probe。
         */
        installTiming: {
            hooksInstalledAt: number;
            hooksLastInstalledAt: number;
            hooksInstallCount: number;
            nowAtProbe: number;
            hasHooksAlias: boolean;
            hasGridAlias: boolean;
        };
    };
    findCellAreasByName: (taskName: string, columnDisplayName: string, options?: {
        scrollIfNeeded?: boolean;
        exact?: boolean;
    }) => {
        ok: boolean;
        reason?: 'row-not-found' | 'column-not-found' | 'row-not-visible' | 'cell-not-found';
        rowId?: string | number;
        columnKey?: string;
        areas?: {
            cell: {
                canvasX: number;
                canvasY: number;
                width: number;
                height: number;
            };
            editIcon: {
                canvasX: number;
                canvasY: number;
                width: number;
                height: number;
            } | null;
            linkIcon: {
                canvasX: number;
                canvasY: number;
                width: number;
                height: number;
            } | null;
            errorIcon: {
                canvasX: number;
                canvasY: number;
                width: number;
                height: number;
            } | null;
            reminderTriangle: {
                canvasX: number;
                canvasY: number;
                width: number;
                height: number;
            } | null;
            viewerAreas: Array<{
                type: string;
                bounds: {
                    canvasX: number;
                    canvasY: number;
                    width: number;
                    height: number;
                };
                value?: any;
            }>;
            meta: {
                viewerType: string;
                readonly: boolean;
                isLink: boolean;
                hasError: boolean;
                hasReminder: boolean;
                hasNestedGrid: boolean;
            };
        } | null;
    };
    /**
     * 直接驱动 onExternalFileDrop callback，绕过浏览器 native HTML5 DataTransfer。
     *
     * 背景：Playwright 的 DataTransfer 对 file payload 历来不稳（`AmE2ETest` 既有
     * `deliverable-upload-download.spec.ts` L760+ 已经因此改成 `setInputFiles`）。
     * 这里给 e2e 一条**和真实 drop 完全等价**的入口：与 EventManager.handleExternalFileDrop
     * 调用同一个 callback，能跑通业务侧 onCanvasExternalFileDrop 的全链路。
     *
     * 参数：
     *   - rowId       : 行的用户 id（与 getRowData 入参一致）
     *   - columnKey   : 列 key（与 getColumnConfig / getCellLayout 入参一致）
     *   - files       : 浏览器侧构造的 File[]（spec 用 `new File([bytes], name, {type})`）
     *
     * 返回：
     *   - true        : 命中 row + col 且 onExternalFileDrop callback 已被触发
     *   - false       : row 或 column 找不到 / callback 未注册（spec 应当判 false=失败）
     */
    simulateExternalFileDrop: (rowId: string | number, columnKey: string, files: File[]) => boolean;
    /**
     * 与 simulateExternalFileDrop 对称：直接驱动 onExternalFilePaste callback，
     * 绕过浏览器 native ClipboardEvent。语义为"追加上传"，不清除 cell 现有文件。
     * 返回 true=callback 已触发，false=row/col 找不到或 callback 未注册。
     */
    simulateExternalFilePaste: (rowId: string | number, columnKey: string, files: File[]) => boolean;
    /**
     * 读取指定 cell 当前传给 viewer.draw 的 data 对象的浅拷贝（含 uploadingItems）。
     *
     * 用于 e2e 断言上传占位是否成功插入 / 进度环渲染数据是否回写到位。
     * 仅暴露 viewer 渲染相关的几个字段，避免把整个内部对象 dump 到 spec 端。
     *
     * 返回：null 表示 row 或 column 找不到 / cell 还没绘制过。
     */
    getCellViewerData: (rowId: string | number, columnKey: string) => {
        value: any;
        uploadingItems: Array<{
            id: string;
            name?: string;
            previewUrl?: string;
            progress?: number;
        }> | undefined;
        status: any;
    } | null;
}
/**
 * Test info for a single nested grid instance
 */
export interface NestedGridTestInfo {
    /** Parent row ID */
    parentRowId: string | number;
    /** Parent row index */
    parentRowIndex: number;
    /** Parent column key */
    parentColKey: string;
    /** Parent column index */
    parentColIndex: number;
    /**
     * Parent column display name（取自 column.name / title / label，便于 spec 按"配置/美宣"等业务名匹配）。
     * 如果都没配置，可能是空字符串。
     */
    parentColName: string;
    /** Nested grid position/size relative to canvas */
    bounds: {
        x: number;
        y: number;
        width: number;
        height: number;
    };
    /** Number of data rows in the nested grid */
    dataRowCount: number;
    /** Number of columns in the nested grid */
    columnCount: number;
    /** Nested grid column keys */
    columnKeys: string[];
    /** Current selection range (null if no selection) */
    selectionRange: {
        startRow: number;
        startCol: number;
        endRow: number;
        endCol: number;
    } | null;
    /** Selected cell (single click, legacy) */
    selectedCell: {
        row: number;
        col: number;
    } | null;
    /** Additional selected cells (Ctrl+Click) */
    additionalSelectedCells: string[];
    /** Whether the nested grid is expanded */
    expanded: boolean;
    /** Whether a cell is being edited */
    editingCell: {
        row: number;
        col: number;
    } | null;
    /** Hovered cell */
    hoveredCell: {
        row: number;
        col: number;
    } | null;
    /** Row height */
    rowHeight: number;
    /**
     * 该嵌套表的 NestedGrid 实例是否已创建（即至少 draw 过一次）。
     *   - true ：所有字段（bounds/selectionRange/...）来自实例，可直接交互
     *   - false：实例尚未 lazy-create，bounds/dataRowCount/columnCount 由配置推导，
     *           交互前需要让 cell 进入视口并触发一次 draw
     */
    rendered: boolean;
}
/**
 * Bounding box in canvas coordinates (can be converted to viewport coords)
 */
export interface AreaBounds {
    /** Canvas-relative X coordinate */
    canvasX: number;
    /** Canvas-relative Y coordinate */
    canvasY: number;
    /** Width in pixels */
    width: number;
    /** Height in pixels */
    height: number;
}
/**
 * A viewer-specific interactive area within a cell
 */
export interface ViewerArea {
    /** Area type identifier */
    type: string;
    /** Area bounds */
    bounds: AreaBounds;
    /** Current value (e.g. boolean true/false, progress 0.75) */
    value?: any;
}
/**
 * All interactive areas within a single cell.
 * Used by Playwright to know WHERE to click for specific interactions.
 */
export interface CellInteractiveAreas {
    /** Entire cell area */
    cell: AreaBounds;
    /** Edit icon (hover-triggered, top-right) — null if readonly/hideEditIcon/nestedGrid */
    editIcon: AreaBounds | null;
    /** Link icon — null if not a link column */
    linkIcon: AreaBounds | null;
    /** Error/warning icon — null if no error */
    errorIcon: AreaBounds | null;
    /** Reminder triangle (top-right corner) — null if no reminder */
    reminderTriangle: AreaBounds | null;
    /** Viewer-specific interactive areas */
    viewerAreas: ViewerArea[];
    /** Cell metadata */
    meta: {
        viewerType: string;
        readonly: boolean;
        isLink: boolean;
        hasError: boolean;
        hasReminder: boolean;
        hasNestedGrid: boolean;
    };
}
/**
 * Interactive areas within a column header
 */
export interface HeaderInteractiveAreas {
    /** Entire header cell area */
    header: AreaBounds;
    /** Sort icon area (shown on hover, left of filter icon) — null for compound headers */
    sortIcon: AreaBounds | null;
    /** Filter icon area (shown on hover, rightmost) — null for compound headers */
    filterIcon: AreaBounds | null;
    /** Column resize handle (right edge ±6px) — null for compound headers */
    resizeHandle: AreaBounds;
    /** Settings icon (nested parent header only) */
    settingsIcon: AreaBounds | null;
    /** Metadata */
    meta: {
        colId: string;
        columnKey: string;
        colType: string;
        index: number;
        sortDisabled: boolean;
        canDrag: boolean;
        isNested: boolean;
    };
}
/**
 * Interactive areas within a group row header
 */
export interface GroupRowAreas {
    /** Expand/collapse arrow */
    expandArrow: AreaBounds;
    /** Checkbox area — null if not a leaf group or no checkbox */
    checkbox: AreaBounds | null;
    /** Entire row area */
    row: AreaBounds;
    /** Metadata */
    meta: {
        expanded: boolean;
        groupLevel: number;
        groupName: string;
        isLeafGroup: boolean;
        rowId: string;
    };
}
/**
 * AddTaskRow（⊕ 新建任务行）的 canvas 几何信息。
 *
 * task=be29ce06：`任务表格最后一行+号快速创建任务` 这个 e2e case 在 canvas 模式下
 * 必须能精确点中 ⊕ 圆心，命中 `AddTaskRow.isInsideIconArea` 触发
 * `grid.onAddRow → onCreateNewRow`。原本 spec 端按 `originFixedWidth/2 + 8` 估算
 * 在 1D / 2D / 3D 分组下分别偏离 0 / 4 / 14 px，会落到行号区，触发 onRowClick 而
 * 不是 onAddRow——本接口暴露的几何与 `AddTaskRow.isInsideIconArea` 一字不差对齐
 * （含 `hitPadding=4`），保证"e2e 鼠标命中点 = AddTaskRow 运行时判定命中点"。
 */
export interface AddTaskRowArea {
    /** AddTaskRow._rowId（一般是 `add_task_row_<rowIndex>` 或 String(data.id)） */
    rowId: string;
    /** body.data 内的 rowIndex（spec 调试 / log 用，不参与稳定 id 比较） */
    rowIndex: number;
    /** 行所属分组 id（非分组模式 / 该行无 groupId 时为 null） */
    groupId: string | number | null;
    /** _parentGroupLevel（非分组模式或未填时为 null；2D 第一层 = 0，第二层 = 1） */
    parentGroupLevel: number | null;
    /** 整行的 canvas 几何（行号 + 数据区，含右侧 verticalScrollerSize 之外） */
    rowArea: AreaBounds;
    /**
     * ⊕ 图标命中区域（16×16 + hitPadding=4 → 实际 24×24），canvas 相对坐标。
     *
     * 几何与 `AddTaskRow.isInsideIconArea` 一字不差对齐：
     *   bw = parentGroupLevel >= 0
     *     ? maxGroupLevel === 0 ? 0
     *       : (parentGroupLevel >= 2 ? l0w + l1w
     *          : parentGroupLevel >= 1
     *            ? (maxGroupLevel === 1 ? l0w : l0w + l1w)
     *            : l0w)
     *     : 0
     *   iconX = bw + (originFixedWidth - bw - 16) / 2
     *   center = (iconX + 8, rowAbsY + height/2)
     *
     * spec 拿到后用中心点 (canvasX + width/2, canvasY + height/2) → canvasToViewport
     * → page.mouse.click 即可命中。
     */
    iconArea: AreaBounds;
}
/**
 * Row checkbox area for normal (non-group) data rows.
 *
 * 与 `GroupRowAreas.checkbox` 区分：那个是组头行的 checkbox；本接口是普通数据
 * 行 RowHeader 内部的 checkbox。两者命中检测代码各走一套路径，几何也不一样。
 */
export interface RowCheckboxArea {
    /** Canvas-relative X coordinate (16×16 px square). */
    canvasX: number;
    /** Canvas-relative Y coordinate. */
    canvasY: number;
    /** Width — always 16 (CHECKBOX_SIZE). */
    width: number;
    /** Height — always 16 (CHECKBOX_SIZE). */
    height: number;
    meta: {
        /** Row index in body.data. */
        rowIndex: number;
        /** Whether the checkbox is currently rendered as checked. */
        checked: boolean;
        /** Grid maxGroupLevel snapshot (for diagnostic purposes only). */
        maxGroupLevel: number;
        /** Parent group level if this row is inside a group, null otherwise. */
        parentGroupLevel: number | null;
    };
}
/**
 * Current selection/editing state of the table
 */
export interface SelectionState {
    /** Selection range (null if nothing selected) */
    range: {
        startRow: number;
        startCol: number;
        endRow: number;
        endCol: number;
    } | null;
    /** Active (focused) cell */
    activeCell: {
        rowIndex: number;
        colIndex: number;
    } | null;
    /** IDs of checked (checkbox-selected) rows */
    checkedRowIds: (string | number)[];
    /** Editor state — null if editor is closed */
    editor: {
        show: boolean;
        rowIndex: number;
        colIndex: number;
    } | null;
}
/**
 * Visible group row info (for listing group rows in tests)
 */
export interface VisibleGroupRowInfo {
    rowId: string;
    rowIndex: number;
    expanded: boolean;
    groupName: string;
    groupLevel: number;
    isLeafGroup: boolean;
}
/**
 * Registry for multiple DataGrid test hook instances.
 * Supports one page having multiple DataGrid components.
 */
export interface TestHooksRegistry {
    /** Register a grid's hooks (called by installTestHooks) */
    register(canvasId: string, hooks: TestHooksAPI): void;
    /** Unregister a grid's hooks (called on component destroy) */
    unregister(canvasId: string): void;
    /** Get hooks for a specific grid */
    get(canvasId: string): TestHooksAPI | undefined;
    /** Get the first registered grid's hooks (backward compat) */
    getFirst(): TestHooksAPI | undefined;
    /** List all registered canvas IDs */
    list(): string[];
}
/**
 * Global window augmentation for test hooks
 */
declare global {
    interface Window {
        __CANVAS_TABLE_TEST_HOOKS__?: TestHooksAPI;
        __CANVAS_TABLE_TEST_HOOKS_REGISTRY__?: TestHooksRegistry;
        /**
         * task=f479647b/iter#3：installTestHooks 首次成功安装时的 Date.now()。
         * spec 端可 `await page.waitForFunction(() => typeof window.__CANVAS_TABLE_TEST_HOOKS_INSTALLED_AT__ === 'number')`
         * 等待 hooks 真正就绪，避免"isCanvasReady 早调一拍"的 race。
         */
        __CANVAS_TABLE_TEST_HOOKS_INSTALLED_AT__?: number;
        /** task=f479647b/iter#3：最近一次 installTestHooks 时的 Date.now()（HMR / 多 grid 持续刷新）。 */
        __CANVAS_TABLE_TEST_HOOKS_LAST_INSTALLED_AT__?: number;
        /** task=f479647b/iter#3：installTestHooks 累计调用次数。 */
        __CANVAS_TABLE_TEST_HOOKS_INSTALL_COUNT__?: number;
    }
}
