diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index a26a63c1..05ef6129 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -516,7 +516,7 @@ fn sanitizeValue(self: *Input, value: []const u8, page: *Page) ![]const u8 { .month => return if (isValidMonth(value)) value else "", .week => return if (isValidWeek(value)) value else "", .time => return if (isValidTime(value)) value else "", - .@"datetime-local" => return try sanitizeDatetimeLocal(value, page), + .@"datetime-local" => return try sanitizeDatetimeLocal(value, page.call_arena), .number => return if (isValidFloatingPoint(value)) value else "", .range => return if (isValidFloatingPoint(value)) value else "50", .color => { @@ -667,7 +667,7 @@ fn isValidTime(value: []const u8) bool { /// Sanitize datetime-local: validate and normalize, or return "". /// Spec: if valid, normalize to "YYYY-MM-DDThh:mm" (shortest time form); /// otherwise set to "". -fn sanitizeDatetimeLocal(value: []const u8, page: *Page) ![]const u8 { +fn sanitizeDatetimeLocal(value: []const u8, arena: std.mem.Allocator) ![]const u8 { if (value.len < 16) return ""; // Find separator (T or space) by scanning for it before a valid time start @@ -712,7 +712,7 @@ fn sanitizeDatetimeLocal(value: []const u8, page: *Page) ![]const u8 { const time_len: usize = if (need_seconds) (if (frac_end > 0) 9 + frac_end else 8) else 5; const total_len = date_part.len + 1 + time_len; - const result = try page.call_arena.alloc(u8, total_len); + const result = try arena.alloc(u8, total_len); @memcpy(result[0..date_part.len], date_part); result[date_part.len] = 'T'; @memcpy(result[date_part.len + 1 ..][0..5], time_part[0..5]); @@ -957,3 +957,135 @@ test "WebApi: HTML.Input" { try testing.htmlRunner("element/html/input_radio.html", .{}); try testing.htmlRunner("element/html/input-attrs.html", .{}); } + +test "isValidFloatingPoint" { + // Valid + try std.testing.expect(isValidFloatingPoint("1")); + try std.testing.expect(isValidFloatingPoint("0.5")); + try std.testing.expect(isValidFloatingPoint("-1")); + try std.testing.expect(isValidFloatingPoint("-0.5")); + try std.testing.expect(isValidFloatingPoint("1e10")); + try std.testing.expect(isValidFloatingPoint("1E10")); + try std.testing.expect(isValidFloatingPoint("1e+10")); + try std.testing.expect(isValidFloatingPoint("1e-10")); + try std.testing.expect(isValidFloatingPoint("0.123")); + try std.testing.expect(isValidFloatingPoint(".5")); + // Invalid + try std.testing.expect(!isValidFloatingPoint("")); + try std.testing.expect(!isValidFloatingPoint("+1")); + try std.testing.expect(!isValidFloatingPoint("1.")); + try std.testing.expect(!isValidFloatingPoint("Infinity")); + try std.testing.expect(!isValidFloatingPoint("NaN")); + try std.testing.expect(!isValidFloatingPoint(" 1")); + try std.testing.expect(!isValidFloatingPoint("1 ")); + try std.testing.expect(!isValidFloatingPoint("1e")); + try std.testing.expect(!isValidFloatingPoint("1e+")); + try std.testing.expect(!isValidFloatingPoint("2e308")); // overflow +} + +test "isValidDate" { + try std.testing.expect(isValidDate("2024-01-01")); + try std.testing.expect(isValidDate("2024-02-29")); // leap year + try std.testing.expect(isValidDate("2024-12-31")); + try std.testing.expect(isValidDate("10000-01-01")); // >4-digit year + try std.testing.expect(!isValidDate("2024-02-30")); // invalid day + try std.testing.expect(!isValidDate("2023-02-29")); // not leap year + try std.testing.expect(!isValidDate("2024-13-01")); // invalid month + try std.testing.expect(!isValidDate("2024-00-01")); // month 0 + try std.testing.expect(!isValidDate("0000-01-01")); // year 0 + try std.testing.expect(!isValidDate("2024-1-01")); // single-digit month + try std.testing.expect(!isValidDate("")); + try std.testing.expect(!isValidDate("not-a-date")); +} + +test "isValidMonth" { + try std.testing.expect(isValidMonth("2024-01")); + try std.testing.expect(isValidMonth("2024-12")); + try std.testing.expect(!isValidMonth("2024-00")); + try std.testing.expect(!isValidMonth("2024-13")); + try std.testing.expect(!isValidMonth("0000-01")); + try std.testing.expect(!isValidMonth("")); +} + +test "isValidWeek" { + try std.testing.expect(isValidWeek("2024-W01")); + try std.testing.expect(isValidWeek("2024-W52")); + try std.testing.expect(isValidWeek("2020-W53")); // 2020 has 53 weeks + try std.testing.expect(!isValidWeek("2024-W00")); + try std.testing.expect(!isValidWeek("2024-W54")); + try std.testing.expect(!isValidWeek("0000-W01")); + try std.testing.expect(!isValidWeek("")); +} + +test "isValidTime" { + try std.testing.expect(isValidTime("00:00")); + try std.testing.expect(isValidTime("23:59")); + try std.testing.expect(isValidTime("12:30:45")); + try std.testing.expect(isValidTime("12:30:45.1")); + try std.testing.expect(isValidTime("12:30:45.12")); + try std.testing.expect(isValidTime("12:30:45.123")); + try std.testing.expect(!isValidTime("24:00")); + try std.testing.expect(!isValidTime("12:60")); + try std.testing.expect(!isValidTime("12:30:60")); + try std.testing.expect(!isValidTime("12:30:45.1234")); // >3 frac digits + try std.testing.expect(!isValidTime("12:30:45.")); // dot without digits + try std.testing.expect(!isValidTime("")); +} + +test "sanitizeDatetimeLocal" { + const allocator = std.testing.allocator; + // Already normalized — returns input slice, no allocation + try std.testing.expectEqualStrings("2024-01-01T12:30", try sanitizeDatetimeLocal("2024-01-01T12:30", allocator)); + // Space separator → T (allocates) + { + const result = try sanitizeDatetimeLocal("2024-01-01 12:30", allocator); + try std.testing.expectEqualStrings("2024-01-01T12:30", result); + allocator.free(result); + } + // Strip trailing :00 (allocates) + { + const result = try sanitizeDatetimeLocal("2024-01-01T12:30:00", allocator); + try std.testing.expectEqualStrings("2024-01-01T12:30", result); + allocator.free(result); + } + // Keep non-zero seconds (allocates) + { + const result = try sanitizeDatetimeLocal("2024-01-01T12:30:45", allocator); + try std.testing.expectEqualStrings("2024-01-01T12:30:45", result); + allocator.free(result); + } + // Keep fractional seconds, strip trailing zeros (allocates) + { + const result = try sanitizeDatetimeLocal("2024-01-01T12:30:45.100", allocator); + try std.testing.expectEqualStrings("2024-01-01T12:30:45.1", result); + allocator.free(result); + } + // Invalid → "" (no allocation) + try std.testing.expectEqualStrings("", try sanitizeDatetimeLocal("not-a-datetime", allocator)); + try std.testing.expectEqualStrings("", try sanitizeDatetimeLocal("", allocator)); +} + +test "parseAllDigits" { + try std.testing.expectEqual(@as(?u32, 0), parseAllDigits("0")); + try std.testing.expectEqual(@as(?u32, 123), parseAllDigits("123")); + try std.testing.expectEqual(@as(?u32, 2024), parseAllDigits("2024")); + try std.testing.expectEqual(@as(?u32, null), parseAllDigits("")); + try std.testing.expectEqual(@as(?u32, null), parseAllDigits("12a")); + try std.testing.expectEqual(@as(?u32, null), parseAllDigits("abc")); +} + +test "daysInMonth" { + try std.testing.expectEqual(@as(u32, 31), daysInMonth(2024, 1)); + try std.testing.expectEqual(@as(u32, 29), daysInMonth(2024, 2)); // leap + try std.testing.expectEqual(@as(u32, 28), daysInMonth(2023, 2)); // non-leap + try std.testing.expectEqual(@as(u32, 30), daysInMonth(2024, 4)); + try std.testing.expectEqual(@as(u32, 29), daysInMonth(2000, 2)); // century leap + try std.testing.expectEqual(@as(u32, 28), daysInMonth(1900, 2)); // century non-leap +} + +test "maxWeeksInYear" { + try std.testing.expectEqual(@as(u32, 52), maxWeeksInYear(2024)); + try std.testing.expectEqual(@as(u32, 53), maxWeeksInYear(2020)); // Jan 1 = Wed + leap + try std.testing.expectEqual(@as(u32, 53), maxWeeksInYear(2015)); // Jan 1 = Thu + try std.testing.expectEqual(@as(u32, 52), maxWeeksInYear(2023)); +}