Add timezone support (#20493)

This commit is contained in:
Pranav K 2020-04-14 13:37:49 -07:00 committed by GitHub
parent 468af978a3
commit 3fbb2f0cd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 186 additions and 83 deletions

View File

@ -9,9 +9,9 @@
-->
<Dependencies>
<ProductDependencies>
<Dependency Name="Microsoft.AspNetCore.Components.WebAssembly.Runtime" Version="3.2.0-preview5.20210.3">
<Dependency Name="Microsoft.AspNetCore.Components.WebAssembly.Runtime" Version="3.2.0-preview5.20213.2">
<Uri>https://github.com/dotnet/blazor</Uri>
<Sha>f429c0dd1a5dc84a70c66091460747d52475874f</Sha>
<Sha>e3ae74171b19c8dbd70e94cf8ac3024a1125479f</Sha>
</Dependency>
<Dependency Name="System.Net.Http.Json" Version="3.2.0-preview5.20210.3">
<Uri>https://github.com/dotnet/corefx</Uri>

View File

@ -99,7 +99,7 @@
<!-- Only listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 -->
<MicrosoftNETCorePlatformsPackageVersion>3.1.0</MicrosoftNETCorePlatformsPackageVersion>
<!-- Packages from aspnet/Blazor -->
<MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>3.2.0-preview5.20210.3</MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>
<MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>3.2.0-preview5.20213.2</MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>
<!-- Packages from aspnet/Extensions -->
<InternalAspNetCoreAnalyzersPackageVersion>3.1.3-servicing.20128.2</InternalAspNetCoreAnalyzersPackageVersion>
<MicrosoftAspNetCoreAnalyzerTestingPackageVersion>3.1.3-servicing.20128.2</MicrosoftAspNetCoreAnalyzerTestingPackageVersion>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,47 @@
const uint64HighPartShift = Math.pow(2, 32);
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
export function readInt32LE(buffer: Uint8Array, position: number): any {
return (buffer[position])
| (buffer[position + 1] << 8)
| (buffer[position + 2] << 16)
| (buffer[position + 3] << 24);
}
export function readUint32LE(buffer: Uint8Array, position: number): any {
return (buffer[position])
+ (buffer[position + 1] << 8)
+ (buffer[position + 2] << 16)
+ ((buffer[position + 3] << 24) >>> 0); // The >>> 0 coerces the value to unsigned
}
export function readUint64LE(buffer: Uint8Array, position: number): any {
// This cannot be done using bit-shift operators in JavaScript, because
// those all implicitly convert to int32
const highPart = readUint32LE(buffer, position + 4);
if (highPart > maxSafeNumberHighPart) {
throw new Error(`Cannot read uint64 with high order part ${highPart}, because the result would exceed Number.MAX_SAFE_INTEGER.`);
}
return (highPart * uint64HighPartShift) + readUint32LE(buffer, position);
}
export function readLEB128(buffer: Uint8Array, position: number) {
let result = 0;
let shift = 0;
for (let index = 0; index < 4; index++) {
const byte = buffer[position + index];
result |= (byte & 127) << shift;
if (byte < 128) {
break;
}
shift += 7;
}
return result;
}
export function numLEB128Bytes(value: number) {
return value < 128 ? 1
: value < 16384 ? 2
: value < 2097152 ? 3 : 4;
}

View File

@ -2,6 +2,7 @@ import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
import { showErrorNotification } from '../../BootErrors';
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
import { Platform, System_Array, Pointer, System_Object, System_String } from '../Platform';
import { loadTimezoneData } from './TimezoneDataFile';
let mono_string_get_utf8: (managedString: System_String) => Pointer;
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void;
@ -161,7 +162,7 @@ function addGlobalModuleScriptTagsToDocument(callback: () => void) {
function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoader, onReady: () => void, onError: (reason?: any) => void) {
const resources = resourceLoader.bootConfig.resources;
const module = {} as typeof Module;
const module = (window['Module'] || { }) as typeof Module;
const suppressMessages = ['DEBUGGING ENABLED'];
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(line));
@ -170,8 +171,8 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
console.error(line);
showErrorNotification();
};
module.preRun = [];
module.postRun = [];
module.preRun = module.preRun || [];
module.postRun = module.postRun || [];
(module as any).preloadPlugins = [];
// Begin loading the .dll/.pdb/.wasm files, but don't block here. Let other loading processes run in parallel.
@ -183,6 +184,15 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
/* url */ `_framework/wasm/${dotnetWasmResourceName}`,
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName]);
const dotnetTimeZoneResourceName = 'dotnet.timezones.dat';
let timeZoneResource: LoadingResource | undefined;
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(dotnetTimeZoneResourceName)) {
timeZoneResource = resourceLoader.loadResource(
dotnetTimeZoneResourceName,
`_framework/wasm/${dotnetTimeZoneResourceName}`,
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName]);
}
// Override the mechanism for fetching the main wasm file so we can connect it to our cache
module.instantiateWasm = (imports, successCallback): WebAssembly.Exports => {
(async () => {
@ -206,6 +216,10 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
MONO.loaded_files = [];
if (timeZoneResource) {
loadTimezone(timeZoneResource);
}
// Fetch the assemblies and PDBs in the background, telling Mono to wait until they are loaded
// Mono requires the assembly filenames to have a '.dll' extension, so supply such names regardless
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
@ -339,6 +353,17 @@ function attachInteropInvoker(): void {
});
}
async function loadTimezone(timeZoneResource: LoadingResource) : Promise<void> {
const runDependencyId = `blazor:timezonedata`;
Module.addRunDependency(runDependencyId);
const request = await timeZoneResource.response;
const arrayBuffer = await request.arrayBuffer();
loadTimezoneData(arrayBuffer)
Module.removeRunDependency(runDependencyId);
}
async function compileWasmModule(wasmResource: LoadingResource, imports: any): Promise<WebAssembly.Instance> {
// This is the same logic as used in emscripten's generated js. We can't use emscripten's js because
// it doesn't provide any method for supplying a custom response provider, and we want to integrate

View File

@ -0,0 +1,44 @@
import { readInt32LE } from "../../BinaryDecoder";
import { decodeUtf8 } from "../../Utf8Decoder";
export async function loadTimezoneData(arrayBuffer: ArrayBuffer) : Promise<void> {
let remainingData = new Uint8Array(arrayBuffer);
// The timezone file is generated by https://github.com/dotnet/blazor/tree/master/src/TimeZoneData.
// The file format of the TZ file look like so
//
// [4 - byte length of manifest]
// [json manifest]
// [data bytes]
//
// The json manifest is an array that looks like so:
//
// [...["America/Fort_Nelson",2249],["America/Glace_Bay",2206]..]
//
// where the first token in each array is the relative path of the file on disk, and the second is the
// length of the file. The starting offset of a file can be calculated using the lengths of all files
// that appear prior to it.
const manifestSize = readInt32LE(remainingData, 0);
remainingData = remainingData.slice(4);
const manifestContent = decodeUtf8(remainingData.slice(0, manifestSize));
const manifest = JSON.parse(manifestContent) as ManifestEntry[];
remainingData = remainingData.slice(manifestSize);
// Create the folder structure
// /zoneinfo
// /zoneinfo/Africa
// /zoneinfo/Asia
// ..
Module['FS_createPath']('/', 'zoneinfo', true, true);
new Set(manifest.map(m => m[0].split('/')![0])).forEach(folder =>
Module['FS_createPath']('/zoneinfo', folder, true, true));
for (const [name, length] of manifest) {
const bytes = new Uint8Array(remainingData.slice(0, length));
Module['FS_createDataFile'](`/zoneinfo/${name}`, null, bytes, true, true, true);
remainingData = remainingData.slice(length);
}
}
type ManifestEntry = [string, number];

View File

@ -1,5 +1,6 @@
import { RenderBatch, ArrayRange, RenderTreeDiff, ArrayValues, RenderTreeEdit, EditType, FrameType, RenderTreeFrame, RenderTreeDiffReader, RenderTreeFrameReader, RenderTreeEditReader, ArrayRangeReader, ArrayBuilderSegmentReader, ArrayBuilderSegment } from './RenderBatch';
import { decodeUtf8 } from './Utf8Decoder';
import { decodeUtf8 } from '../../Utf8Decoder';
import { readInt32LE, readUint64LE, readLEB128, numLEB128Bytes } from '../../BinaryDecoder';
const updatedComponentsEntryLength = 4; // Each is a single int32 giving the location of the data
const referenceFramesEntryLength = 20; // 1 int for frame type, then 16 bytes for type-specific data
@ -7,8 +8,6 @@ const disposedComponentIdsEntryLength = 4; // Each is an int32 giving the ID
const disposedEventHandlerIdsEntryLength = 8; // Each is an int64 giving the ID
const editsEntryLength = 16; // 4 ints
const stringTableEntryLength = 4; // Each is an int32 giving the string data location, or -1 for null
const uint64HighPartShift = Math.pow(2, 32);
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
export class OutOfProcessRenderBatch implements RenderBatch {
constructor(private batchData: Uint8Array) {
@ -230,47 +229,4 @@ class OutOfProcessArrayBuilderSegmentReader implements ArrayBuilderSegmentReader
}
}
function readInt32LE(buffer: Uint8Array, position: number): any {
return (buffer[position])
| (buffer[position + 1] << 8)
| (buffer[position + 2] << 16)
| (buffer[position + 3] << 24);
}
function readUint32LE(buffer: Uint8Array, position: number): any {
return (buffer[position])
+ (buffer[position + 1] << 8)
+ (buffer[position + 2] << 16)
+ ((buffer[position + 3] << 24) >>> 0); // The >>> 0 coerces the value to unsigned
}
function readUint64LE(buffer: Uint8Array, position: number): any {
// This cannot be done using bit-shift operators in JavaScript, because
// those all implicitly convert to int32
const highPart = readUint32LE(buffer, position + 4);
if (highPart > maxSafeNumberHighPart) {
throw new Error(`Cannot read uint64 with high order part ${highPart}, because the result would exceed Number.MAX_SAFE_INTEGER.`);
}
return (highPart * uint64HighPartShift) + readUint32LE(buffer, position);
}
function readLEB128(buffer: Uint8Array, position: number) {
let result = 0;
let shift = 0;
for (let index = 0; index < 4; index++) {
const byte = buffer[position + index];
result |= (byte & 127) << shift;
if (byte < 128) {
break;
}
shift += 7;
}
return result;
}
function numLEB128Bytes(value: number) {
return value < 128 ? 1
: value < 16384 ? 2
: value < 2097152 ? 3 : 4;
}

View File

@ -82,7 +82,11 @@
<ItemGroup>
<_BlazorJSFile Include="$(_BlazorJSPath)" />
<_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" />
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*"/>
<_DotNetWasmRuntimeFile
Remove="%(Identity)"
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(FileName)%(Extension)' == 'dotnet.timezones.dat'" />
<!--
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.

View File

@ -2,12 +2,14 @@
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".dat" />
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
<remove fileExtension=".wasm" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />

View File

@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var buildOutputDirectory = project.BuildOutputDirectory;
var extensions = new[] { ".dll", ".js", ".pdb", ".wasm", ".map", ".json" };
var extensions = new[] { ".dll", ".js", ".pdb", ".wasm", ".map", ".json", ".dat" };
// Act
var compressedFilesPath = Path.Combine(
project.DirectoryPath,
@ -218,7 +218,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var buildOutputDirectory = project.BuildOutputDirectory;
var extensions = new[] { ".dll", ".js", ".pdb", ".wasm", ".map", ".json" };
var extensions = new[] { ".dll", ".js", ".pdb", ".wasm", ".map", ".json", ".dat" };
// Act
var compressedFilesPath = Path.Combine(
project.DirectoryPath,

View File

@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", DotNetJsFileName);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", DotNetJsFileName);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -81,6 +83,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains(DotNetJsFileName, runtime);
Assert.Contains("dotnet.wasm", runtime);
Assert.Contains("dotnet.timezones.dat", runtime);
var assemblies = bootJsonData.resources.assembly.Keys;
Assert.Contains("standalone.dll", assemblies);
@ -112,6 +115,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains(DotNetJsFileName, runtime);
Assert.Contains("dotnet.wasm", runtime);
Assert.Contains("dotnet.timezones.dat", runtime);
var assemblies = bootJsonData.resources.assembly.Keys;
Assert.Contains("standalone.dll", assemblies);
@ -122,6 +126,35 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.Null(bootJsonData.resources.satelliteResources);
}
[Fact]
public async Task Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZoneInfo()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
project.Configuration = "Release";
project.AddProjectFileContent(
@"
<PropertyGroup>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
var bootJsonPath = Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
var bootJsonData = ReadBootJsonData(result, bootJsonPath);
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.DoesNotContain("dotnet.timezones.dat", runtime);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
}
[Fact]
public async Task Build_Hosted_Works()
{
@ -163,6 +196,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", DotNetJsFileName);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
}

View File

@ -76,6 +76,7 @@ namespace Microsoft.AspNetCore.Builder
// release builds unless BlazorEnableDebugging is explicitly set to true.
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".br", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".dat", MediaTypeNames.Application.Octet);
options.ContentTypeProvider = contentTypeProvider;

View File

@ -26,8 +26,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
}
protected override TimeSpan UtcOffset => TimeZoneInfo.Local.BaseUtcOffset;
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase);

View File

@ -22,13 +22,6 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
protected abstract void SetCulture(string culture);
/// <summary>
/// Blazor Server and these tests will use the application's UtcOffset when calculating DateTimeOffset when
/// an offset is not explicitly specified. Blazor WASM always calculates DateTimeOffsets as Utc.
/// We'll use <see cref="UtcOffset"/> to express this difference in calculating expected values in these tests.
/// </summary>
protected abstract TimeSpan UtcOffset { get; }
[Theory]
[InlineData("en-US")]
[InlineData("fr-FR")]
@ -69,11 +62,11 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
// datetimeoffset
input = Browser.FindElement(By.Id("input_type_text_datetimeoffset"));
display = Browser.FindElement(By.Id("input_type_text_datetimeoffset_value"));
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
input.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
input.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
input.SendKeys("\t");
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
}
// The logic is different for verifying culture-invariant fields. The problem is that the logic for what
@ -140,13 +133,13 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
input = Browser.FindElement(By.Id("input_type_date_datetimeoffset"));
display = Browser.FindElement(By.Id("input_type_date_datetimeoffset_value"));
extraInput = Browser.FindElement(By.Id("input_type_date_datetimeoffset_extrainput"));
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
extraInput.SendKeys("\t");
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
}
[Theory]
@ -209,13 +202,13 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
input = Browser.FindElement(By.Id("inputdate_datetimeoffset"));
display = Browser.FindElement(By.Id("inputdate_datetimeoffset_value"));
extraInput = Browser.FindElement(By.Id("inputdate_datetimeoffset_extrainput"));
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
extraInput.SendKeys("\t");
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
}
}
}

View File

@ -24,8 +24,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
}
protected override TimeSpan UtcOffset => TimeSpan.Zero;
protected override void SetCulture(string culture)
{
Navigate($"{ServerPathBase}/?culture={culture}", noReload: false);