Skip to content
Merged
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
36 changes: 36 additions & 0 deletions .github/workflows/visactor-trusted-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: VisActor trusted CI dispatch
run-name: VisActor trusted CI / ${{ inputs.request_digest }}

# 只受理默认分支的固定控制请求;PR 代码不会在持有 GitHub 凭据的 job 中执行。
on:
workflow_dispatch:
inputs:
request:
description: Version-bound CI request from the trusted controller
required: true
type: string
request_digest:
description: SHA-256 of the canonical request
required: true
type: string

permissions:
contents: read

jobs:
dispatch:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22.22.2'
- name: Confirm the bound request (does not execute tests)
env:
FLOW_CI_REQUEST: ${{ inputs.request }}
FLOW_CI_REQUEST_DIGEST: ${{ inputs.request_digest }}
run: node common/scripts/flow-ci-dispatch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "修复重复提交同一选区时新选框被误删、旧选框与填充柄残留的问题。",
"type": "patch",
"packageName": "@visactor/vtable"
}
],
"packageName": "@visactor/vtable",
"email": "sunrui1992@foxmail.com"
}
46 changes: 46 additions & 0 deletions common/config/flow-ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"schemaVersion": 1,
"kind": "trusted_ci_policy",
"repository": "VisActor/VTable",
"baseRef": "develop",
"nodeMajor": 22,
"rushVersion": "5.148.0",
"pnpmVersion": "10.7.0",
"unitPackages": [
"react-vtable",
"vtable",
"vtable-editors",
"vtable-gantt",
"vtable-plugins",
"vtable-search",
"vtable-sheet",
"vue-vtable"
],
"buildTargets": [
"@visactor/react-vtable",
"@visactor/vtable",
"@visactor/vtable-editors",
"@visactor/vtable-gantt",
"@visactor/vtable-plugins",
"@visactor/vtable-search",
"@visactor/vtable-sheet",
"@visactor/vue-vtable",
"@visactor/vtable-export",
"@visactor/vtable-calendar"
],
"visualProduct": "vtable",
"visualSelection": "full",
"requiredChecks": [
"VisActor Flow / Build",
"VisActor Flow / Unit",
"VisActor Flow / Repository visual"
],
"firefoxPaste": true,
"visualSuites": [
"core"
],
"buildFormats": [
"es",
"cjs"
]
}
48 changes: 19 additions & 29 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions common/scripts/flow-ci-dispatch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

/** 固定键顺序;派发摘要绑定完整输入,不依赖作者提供的展示标题。 */
export function canonical(value) {
if (Array.isArray(value)) return value.map(canonical);
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
return value;
}

/** 默认分支只确认受理身份;候选源码、构建及测试由无凭据隔离侧执行。 */
export function dispatchReceipt(text, expectedDigest, policy, env) {
if (typeof text !== 'string' || Buffer.byteLength(text) > 4096) throw new Error('派发输入超限');
const request = JSON.parse(text);
const keys = ['schemaVersion', 'kind', 'requestId', 'repository', 'pullRequest', 'headSha', 'baseSha', 'controlCommit', 'policyDigest', 'dependencyDigest'];
const sha = value => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
if (!request || Array.isArray(request) || Object.keys(request).sort().join() !== keys.sort().join() ||
request.schemaVersion !== 1 || request.kind !== 'ci_request' || !hash(request.requestId) ||
request.repository !== policy.repository || !Number.isSafeInteger(request.pullRequest) || request.pullRequest < 1 ||
![request.headSha, request.baseSha, request.controlCommit].every(sha) || ![request.policyDigest, request.dependencyDigest, expectedDigest].every(hash)) throw new Error('派发身份非法');
const requestDigest = createHash('sha256').update(JSON.stringify(canonical(request))).digest('hex');
if (requestDigest !== expectedDigest || env.GITHUB_EVENT_NAME !== 'workflow_dispatch' ||
env.GITHUB_REPOSITORY !== policy.repository || env.GITHUB_REF !== 'refs/heads/' + policy.baseRef ||
env.GITHUB_SHA !== request.controlCommit || env.GITHUB_RUN_ATTEMPT !== '1' || !/^[1-9][0-9]*$/.test(env.GITHUB_RUN_ID ?? '')) throw new Error('派发不是当前可信默认分支的首次运行');
return { schemaVersion: 1, kind: 'ci_dispatch_receipt', requestDigest, request, workflowRunId: env.GITHUB_RUN_ID,
controlCommit: env.GITHUB_SHA, testsExecuted: false };
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const policy = JSON.parse(await fs.readFile(new URL('../config/flow-ci.json', import.meta.url), 'utf8'));
console.log(JSON.stringify(dispatchReceipt(process.env.FLOW_CI_REQUEST, process.env.FLOW_CI_REQUEST_DIGEST, policy, process.env)));
}
46 changes: 46 additions & 0 deletions common/scripts/flow-ci-dispatch.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import { canonical, dispatchReceipt } from './flow-ci-dispatch.mjs';

const policy = JSON.parse(await fs.readFile(new URL('../config/flow-ci.json', import.meta.url), 'utf8'));
const request = { schemaVersion: 1, kind: 'ci_request', requestId: 'a'.repeat(64), repository: policy.repository, pullRequest: 1,
headSha: 'b'.repeat(40), baseSha: 'c'.repeat(40), controlCommit: 'd'.repeat(40), policyDigest: 'e'.repeat(64), dependencyDigest: 'f'.repeat(64) };
const hash = value => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
const env = { GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: policy.repository, GITHUB_REF: 'refs/heads/develop',
GITHUB_SHA: request.controlCommit, GITHUB_RUN_ATTEMPT: '1', GITHUB_RUN_ID: '123' };

test('仅受理固定默认分支和精确输入,不宣称执行了测试', () => {
const result = dispatchReceipt(JSON.stringify(request), hash(request), policy, env);
assert.equal(result.testsExecuted, false); assert.equal(result.requestDigest, hash(request));
assert.equal(result.workflowRunId, '123'); assert.deepEqual(result.request, request);
});

for (const change of [{ GITHUB_EVENT_NAME: 'pull_request_target' }, { GITHUB_EVENT_NAME: 'pull_request' },
{ GITHUB_REF: 'refs/pull/1/merge' }, { GITHUB_REF: 'refs/heads/untrusted' }, { GITHUB_REPOSITORY: 'fork/repository' },
{ GITHUB_SHA: '0'.repeat(40) }, { GITHUB_RUN_ATTEMPT: '2' }, { GITHUB_RUN_ID: '0' }]) test('拒绝不可信运行上下文 ' + JSON.stringify(change), () => {
assert.throws(() => dispatchReceipt(JSON.stringify(request), hash(request), policy, { ...env, ...change }));
});

for (const change of [{ pullRequest: 0 }, { repository: 'fork/repository' }, { headSha: 'main' }, { extra: true },
{ policyDigest: null }, { dependencyDigest: 'x' }, { kind: 'test_result' }]) test('拒绝未绑定输入 ' + JSON.stringify(change), () => {
const value = { ...request, ...change }; assert.throws(() => dispatchReceipt(JSON.stringify(value), hash(value), policy, env));
});

test('摘要变化和超限输入不能复用受理记录', () => {
assert.throws(() => dispatchReceipt(JSON.stringify({ ...request, headSha: '0'.repeat(40) }), hash(request), policy, env));
assert.throws(() => dispatchReceipt(' '.repeat(4097), hash(request), policy, env));
});


test('固定单测清单覆盖当前所有产品包,不因默认分支新增包而漏测', async () => {
const root = new URL('../../packages/', import.meta.url); const expected = [];
for (const entry of await fs.readdir(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const name = entry.name;
try { const pkg = JSON.parse(await fs.readFile(new URL(name + '/package.json', root), 'utf8')); if (pkg.scripts?.test) expected.push(name); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
}
assert.deepEqual([...policy.unitPackages].sort(), expected.sort());
});
38 changes: 27 additions & 11 deletions packages/vtable/__tests__/listTable-highlightInRange.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-nocheck
// 有问题可对照demo unitTestListTable
import records from './data/marketsales.json';
import { ListTable } from '../src';
import { ListTable, setCustomAlphabetCharSet } from '../src';
import { createDiv } from './dom';
global.__VERSION__ = 'none';
describe('listTable init test', () => {
Expand Down Expand Up @@ -200,17 +200,33 @@ describe('listTable init test', () => {
expect(listTable.getScrollTop()).toBe(scrollTop);
});
test('listTable measureTextWidth', () => {
const measureTextWdith = listTable.measureText("家里方大化工撒个福建师大看哈 fdsfgj! *-+&5%#.,'.,。、", {
fontFamily: 'Arial',
fontSize: 15,
fontWeight: 'bold'
});
expect(measureTextWdith).toEqual({
fontBoundingBoxAscent: 14,
fontBoundingBoxDescent: 3,
width: 390.0501427283655,
height: 15
// 固定原生字形度量,精确验证快速测量;不把操作系统字体的小数误差作为 API 契约。
setCustomAlphabetCharSet('');
const measure = jest.spyOn(CanvasRenderingContext2D.prototype, 'measureText').mockImplementation(function (text) {
expect(this.font).toBe('bold 15px Arial');
return {
width: Array.from(text).reduce((width, char) => width + (char.charCodeAt(0) > 127 ? 16 : 8), 0),
fontBoundingBoxAscent: 14,
fontBoundingBoxDescent: 3
};
});
try {
const measureTextWidth = listTable.measureText("家里方大化工撒个福建师大看哈 fdsfgj! *-+&5%#.,'.,。、", {
fontFamily: 'Arial',
fontSize: 15,
fontWeight: 'bold'
});
expect(measure).toHaveBeenCalled();
expect(measureTextWidth).toEqual({
fontBoundingBoxAscent: 14,
fontBoundingBoxDescent: 3,
width: 432,
height: 15
});
} finally {
measure.mockRestore();
setCustomAlphabetCharSet('');
}
});
// test('listTable API getAllCells', () => {
// expect(JSON.parse(JSON.stringify(listTable.getCellInfo(5, 5)))).toEqual({
Expand Down
Loading
Loading