mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(generate): honor clock time bounds in data generation
This commit is contained in:
@@ -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);
|
||||
|
||||
+48
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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(":");
|
||||
}
|
||||
@@ -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 |
Reference in New Issue
Block a user