fix(generate): honor clock time bounds in data generation

This commit is contained in:
Jarvis
2026-10-17 20:09:33 +08:00
committed by GitHub
parent 7abdbf3eb0
commit 0975e1c1bc
8 changed files with 129 additions and 37 deletions
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import { generateTimeInRange } from "@/lib/dataGrid/generatorTimeRange";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
const props = defineProps<{ params: GeneratorParams }>();
@@ -30,12 +31,7 @@ const previewVal = computed(() => {
if (!props.params.allDay) {
return formatLocalDate(d);
}
const sh = parseInt(props.params.startTime ?? "00") || 0;
const sm = parseInt(props.params.endTime ?? "23") || 23;
const h = Math.floor(Math.random() * (sm - sh + 1)) + sh;
const m = Math.floor(Math.random() * 60);
const s = Math.floor(Math.random() * 60);
return `${formatLocalDate(d)} ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${formatLocalDate(d)} ${generateTimeInRange(props.params.startTime, props.params.endTime)}`;
});
const weekdayLabels = computed(() => (["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const).map((d) => t(`dataGenerate.weekdayShort.${d}`)));
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import { generateTimeInRange } from "@/lib/dataGrid/generatorTimeRange";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
const props = defineProps<{ params: GeneratorParams }>();
@@ -12,17 +13,7 @@ const { t } = useI18n();
const previewVal = computed(() => {
void previewKey.value;
let sh = 0,
sm = 23;
if (!props.params.allDay) {
sh = parseInt(props.params.startTime ?? "00") || 0;
sm = parseInt(props.params.endTime ?? "23") || 23;
}
const h = Math.floor(Math.random() * (sm - sh + 1)) + sh;
const m = Math.floor(Math.random() * 60);
const s = Math.floor(Math.random() * 60);
const pad = (n: number) => String(n).padStart(2, "0");
return `${pad(h)}:${pad(m)}:${pad(s)}`;
return props.params.allDay ? generateTimeInRange() : generateTimeInRange(props.params.startTime, props.params.endTime);
});
const previewKey = ref(0);
@@ -0,0 +1,48 @@
// @vitest-environment happy-dom
import { createApp, h, nextTick, reactive } from "vue";
import { createI18n } from "vue-i18n";
import { afterEach, describe, expect, it, vi } from "vitest";
import TimeParams from "../TimeParams.vue";
import DateTimeParams from "../DateTimeParams.vue";
import { generateValue, type GeneratorParams } from "@/lib/dataGrid/dataGenerate";
const cleanups: Array<() => void> = [];
afterEach(() => {
for (const cleanup of cleanups.splice(0)) cleanup();
vi.restoreAllMocks();
});
describe.each(["time", "datetime"] as const)("%s range preview", (kind) => {
it("matches generation and updates when minute bounds are edited", async () => {
vi.spyOn(Math, "random").mockReturnValue(0.5);
const params = reactive<GeneratorParams>({ allDay: kind === "datetime", start: "2026-10-17", end: "2026-10-17", startTime: "09:15", endTime: "09:20" });
const host = document.createElement("div");
document.body.append(host);
const app = createApp({ render: () => h(kind === "time" ? TimeParams : DateTimeParams, { params }) });
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false }));
app.mount(host);
cleanups.push(() => {
app.unmount();
host.remove();
});
const generated = () => String(generateValue("t", kind, kind, 1, params));
expect(generated()).toContain("09:17:30");
expect(host.textContent).toContain(generated());
const inputs = host.querySelectorAll<HTMLInputElement>('input[type="time"]');
expect(inputs).toHaveLength(2);
for (const input of inputs) {
input.value = "00:00";
input.dispatchEvent(new Event("change", { bubbles: true }));
}
await nextTick();
expect(generated()).toContain("00:00:00");
expect(host.textContent).toContain(generated());
const random = vi.spyOn(Math, "random");
random.mockClear();
Array.from(host.querySelectorAll<HTMLButtonElement>("button")).at(-1)?.click();
await nextTick();
expect(random).toHaveBeenCalled();
expect(host.textContent).toContain(generated());
});
});
@@ -0,0 +1,38 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { defaultGeneratorParams, generateValue } from "@/lib/dataGrid/dataGenerate";
// Exercise the production generator, not a copy of the time-range algorithm.
afterEach(() => vi.restoreAllMocks());
describe("time generation bounds", () => {
it.each([
["00:00", "00:00", "00:00:00", "00:00:00", "00:00:00"],
["09:15", "09:20", "09:15:00", "09:17:30", "09:20:00"],
["09:15:10", "09:15:12", "09:15:10", "09:15:11", "09:15:12"],
["09", "09", "09:00:00", "09:30:00", "09:59:59"],
["09:20", "09:15", "09:15:00", "09:17:30", "09:20:00"],
[undefined, undefined, "00:00:00", "12:00:00", "23:59:59"],
["invalid", "25:90", "00:00:00", "12:00:00", "23:59:59"],
])("honors %s..%s for time and daily datetime windows", (startTime, endTime, first, middle, last) => {
for (const [random, expected] of [
[0, first],
[0.5, middle],
[1 - Number.EPSILON, last],
] as const) {
vi.spyOn(Math, "random").mockReturnValue(random);
expect(generateValue("t", "time", "time", 1, { allDay: false, startTime, endTime })).toBe(expected);
expect(generateValue("t", "datetime", "datetime", 1, { allDay: true, start: "2026-10-17", end: "2026-10-17", startTime, endTime })).toBe(`2026-10-17 ${expected}`);
}
});
it("ignores custom bounds in the time generator's all-day mode", () => {
vi.spyOn(Math, "random").mockReturnValue(0.5);
expect(generateValue("t", "time", "time", 1, { allDay: true, startTime: "09:15", endTime: "09:20" })).toBe("12:00:00");
});
it("uses valid time input values for new configurations", () => {
const params = defaultGeneratorParams("t", { dataType: "time", columnDefault: null }, "time");
expect(params.startTime).toBe("00:00");
expect(params.endTime).toBe("23:59");
});
});
+5 -20
View File
@@ -1,5 +1,6 @@
import type { DatabaseType } from "@/types/database";
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { generateTimeInRange } from "@/lib/dataGrid/generatorTimeRange";
import { generateRegexValue } from "@/lib/dataGrid/regexGenerate";
export interface GeneratorNode {
@@ -1434,8 +1435,8 @@ function buildDefaultGeneratorParams(_columnName: string, attrs: ColumnAttrs, ge
}
if (generatorKey === "time") {
params.startTime = "00";
params.endTime = "23";
params.startTime = "00:00";
params.endTime = "23:59";
return params;
}
@@ -1649,13 +1650,7 @@ export function generateValue(columnName: string, dataType: string, generatorKey
if (params?.allDay) {
const [rangeStart, rangeEnd] = ensureRange(start, end);
const d = new Date(rangeStart.getTime() + Math.random() * (rangeEnd.getTime() - rangeStart.getTime()));
const sh = parseInt(params.startTime ?? "00") || 0;
const sm = parseInt(params.endTime ?? "23") || 23;
const h = Math.floor(Math.random() * (sm - sh + 1)) + sh;
const m = Math.floor(Math.random() * 60);
const s = Math.floor(Math.random() * 60);
const pad = (n: number) => String(n).padStart(2, "0");
return `${formatLocalDate(d)} ${pad(h)}:${pad(m)}:${pad(s)}`;
return `${formatLocalDate(d)} ${generateTimeInRange(params.startTime, params.endTime)}`;
}
return randTimestamp(start, end);
}
@@ -1665,17 +1660,7 @@ export function generateValue(columnName: string, dataType: string, generatorKey
return randDate(start, end);
}
if (key === "time") {
let sh = 0,
sm = 23;
if (!params?.allDay) {
sh = parseInt(params?.startTime ?? "00") || 0;
sm = parseInt(params?.endTime ?? "23") || 23;
}
const h = Math.floor(Math.random() * (sm - sh + 1)) + sh;
const m = Math.floor(Math.random() * 60);
const s = Math.floor(Math.random() * 60);
const pad = (n: number) => String(n).padStart(2, "0");
return `${pad(h)}:${pad(m)}:${pad(s)}`;
return params?.allDay ? generateTimeInRange() : generateTimeInRange(params?.startTime, params?.endTime);
}
if (key === "enum") {
const vals = (params?.values ?? "")
@@ -0,0 +1,23 @@
const SECONDS_PER_DAY = 24 * 60 * 60;
function timeBoundSeconds(value: string | undefined, upper: boolean): number {
const fallback = upper ? SECONDS_PER_DAY - 1 : 0;
const match = /^(\d{1,2})(?::(\d{2})(?::(\d{2}))?)?$/.exec(value?.trim() ?? "");
if (!match) return fallback;
const hour = Number(match[1]);
// Older saved configurations contain hours only; retain their whole-hour
// upper bound. Values from the time inputs are exact clock-time bounds.
const minute = match[2] === undefined ? (upper ? 59 : 0) : Number(match[2]);
const second = match[2] === undefined ? (upper ? 59 : 0) : Number(match[3] ?? 0);
if (hour > 23 || minute > 59 || second > 59) return fallback;
return hour * 3600 + minute * 60 + second;
}
export function generateTimeInRange(start?: string, end?: string): string {
const startSeconds = timeBoundSeconds(start, false);
const endSeconds = timeBoundSeconds(end, true);
const lower = Math.min(startSeconds, endSeconds);
const upper = Math.max(startSeconds, endSeconds);
const seconds = lower + Math.floor(Math.random() * (upper - lower + 1));
return [Math.floor(seconds / 3600), Math.floor((seconds % 3600) / 60), seconds % 60].map((part) => String(part).padStart(2, "0")).join(":");
}
+11
View File
@@ -0,0 +1,11 @@
# Time-range generator regression preview
`time-range-preview.png` shows the production `TimeParams.vue` and
`DateTimeParams.vue` components mounted in an isolated local Vite harness, with
sample rows produced by the production `generateValue` function alongside them.
It is a component preview, not a screenshot of a live database operation.
The three examples cover midnight (`00:00` to `00:00`), a minute-level time
window (`09:15` to `09:20`), and the same daily window for datetime generation.
The automated generation and mounted-component tests cover input changes,
refresh, endpoint selection, legacy hour-only configurations, and all-day mode.
Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB