feat: update launch configurations and add outside click handling for date input component
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
bc28d869eb
commit
c0180aab13
7 changed files with 193 additions and 14 deletions
134
.vscode/launch.json
vendored
134
.vscode/launch.json
vendored
|
|
@ -3,7 +3,88 @@
|
||||||
"compounds": [],
|
"compounds": [],
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "WorkTracker: Debug in Docker",
|
"name": "WorkTracker: Debug Docker App + Browser",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
||||||
|
"postDebugTask": "WorkTracker: Docker Debug Down",
|
||||||
|
"program": "/workspace/bin/Debug/net10.0/WorkTracker.dll",
|
||||||
|
"args": [
|
||||||
|
"--urls",
|
||||||
|
"http://+:8080"
|
||||||
|
],
|
||||||
|
"cwd": "/workspace",
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://+:8080",
|
||||||
|
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
|
||||||
|
"UseHttpsRedirection": "false"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/workspace": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"pipeTransport": {
|
||||||
|
"pipeProgram": "docker",
|
||||||
|
"pipeArgs": [
|
||||||
|
"exec",
|
||||||
|
"-i",
|
||||||
|
"worktracker-dev",
|
||||||
|
"sh",
|
||||||
|
"-c"
|
||||||
|
],
|
||||||
|
"debuggerPath": "/vsdbg/vsdbg",
|
||||||
|
"pipeCwd": "${workspaceFolder}",
|
||||||
|
"quoteArgs": false
|
||||||
|
},
|
||||||
|
"serverReadyAction": {
|
||||||
|
"action": "openExternally",
|
||||||
|
"pattern": "\\bNow listening on:\\s+https?://\\S+",
|
||||||
|
"uriFormat": "http://localhost:8002"
|
||||||
|
},
|
||||||
|
"justMyCode": true,
|
||||||
|
"requireExactSource": false,
|
||||||
|
"console": "internalConsole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WorkTracker: Debug Docker App",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
||||||
|
"postDebugTask": "WorkTracker: Docker Debug Down",
|
||||||
|
"program": "/workspace/bin/Debug/net10.0/WorkTracker.dll",
|
||||||
|
"args": [
|
||||||
|
"--urls",
|
||||||
|
"http://+:8080"
|
||||||
|
],
|
||||||
|
"cwd": "/workspace",
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://+:8080",
|
||||||
|
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
|
||||||
|
"UseHttpsRedirection": "false"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/workspace": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"pipeTransport": {
|
||||||
|
"pipeProgram": "docker",
|
||||||
|
"pipeArgs": [
|
||||||
|
"exec",
|
||||||
|
"-i",
|
||||||
|
"worktracker-dev",
|
||||||
|
"sh",
|
||||||
|
"-c"
|
||||||
|
],
|
||||||
|
"debuggerPath": "/vsdbg/vsdbg",
|
||||||
|
"pipeCwd": "${workspaceFolder}",
|
||||||
|
"quoteArgs": false
|
||||||
|
},
|
||||||
|
"justMyCode": true,
|
||||||
|
"requireExactSource": false,
|
||||||
|
"console": "internalConsole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WorkTracker: Debug Docker App + Edge",
|
||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
||||||
|
|
@ -38,58 +119,19 @@
|
||||||
},
|
},
|
||||||
"serverReadyAction": {
|
"serverReadyAction": {
|
||||||
"action": "debugWithEdge",
|
"action": "debugWithEdge",
|
||||||
"pattern": "Now listening on:\\s+https?://\\S+:(\\d+)",
|
"pattern": "\\bNow listening on:\\s+https?://\\S+",
|
||||||
"uriFormat": "http://localhost:8002/?ready=%s"
|
"uriFormat": "http://localhost:8002",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
"console": "internalConsole"
|
"console": "internalConsole"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WorkTracker: Debug App in Docker",
|
"name": "WorkTracker: Launch Integrated Browser for Running Docker App",
|
||||||
"type": "coreclr",
|
"type": "editor-browser",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
"url": "http://localhost:8002"
|
||||||
"postDebugTask": "WorkTracker: Docker Debug Down",
|
|
||||||
"program": "/workspace/bin/Debug/net10.0/WorkTracker.dll",
|
|
||||||
"args": [
|
|
||||||
"--urls",
|
|
||||||
"http://+:8080"
|
|
||||||
],
|
|
||||||
"cwd": "/workspace",
|
|
||||||
"env": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"ASPNETCORE_URLS": "http://+:8080",
|
|
||||||
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
|
|
||||||
"UseHttpsRedirection": "false"
|
|
||||||
},
|
|
||||||
"sourceFileMap": {
|
|
||||||
"/workspace": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
"pipeTransport": {
|
|
||||||
"pipeProgram": "docker",
|
|
||||||
"pipeArgs": [
|
|
||||||
"exec",
|
|
||||||
"-i",
|
|
||||||
"worktracker-dev",
|
|
||||||
"sh",
|
|
||||||
"-c"
|
|
||||||
],
|
|
||||||
"debuggerPath": "/vsdbg/vsdbg",
|
|
||||||
"pipeCwd": "${workspaceFolder}",
|
|
||||||
"quoteArgs": false
|
|
||||||
},
|
|
||||||
"justMyCode": true,
|
|
||||||
"requireExactSource": false,
|
|
||||||
"console": "internalConsole"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "WorkTracker: Debug Edge",
|
|
||||||
"type": "msedge",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:8002",
|
|
||||||
"webRoot": "${workspaceFolder}",
|
|
||||||
"internalConsoleOptions": "neverOpen"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +197,8 @@ else
|
||||||
{
|
{
|
||||||
selectedDate = value.Value;
|
selectedDate = value.Value;
|
||||||
statusMessage = null;
|
statusMessage = null;
|
||||||
|
selectedDay = await WorkDayService.GetAsync(selectedDate);
|
||||||
|
RecomputePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
<div class="localized-date-input" @onkeydown="HandleKeyDown">
|
<div class="localized-date-input" @ref="rootElement" @onkeydown="HandleKeyDown">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="@InputId"
|
<input id="@InputId"
|
||||||
data-testid="@GetInputTestId()"
|
data-testid="@GetInputTestId()"
|
||||||
|
|
@ -75,8 +77,28 @@
|
||||||
private DateOnly? lastValue;
|
private DateOnly? lastValue;
|
||||||
private string displayValue = string.Empty;
|
private string displayValue = string.Empty;
|
||||||
private bool isOpen;
|
private bool isOpen;
|
||||||
|
private bool outsideClickListenerActive;
|
||||||
private DateOnly visibleMonth;
|
private DateOnly visibleMonth;
|
||||||
private IReadOnlyList<CalendarDayCell> calendarDays = [];
|
private IReadOnlyList<CalendarDayCell> calendarDays = [];
|
||||||
|
private ElementReference rootElement;
|
||||||
|
private DotNetObjectReference<LocalizedDateInput>? dotNetReference;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (isOpen && !outsideClickListenerActive)
|
||||||
|
{
|
||||||
|
dotNetReference ??= DotNetObjectReference.Create(this);
|
||||||
|
await JS.InvokeVoidAsync("workTrackerDateInput.registerOutsideClick", rootElement, dotNetReference);
|
||||||
|
outsideClickListenerActive = true;
|
||||||
|
}
|
||||||
|
else if (!isOpen && outsideClickListenerActive)
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement);
|
||||||
|
outsideClickListenerActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
|
|
@ -190,6 +212,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public Task ClosePopupFromOutsideClickAsync()
|
||||||
|
{
|
||||||
|
if (!isOpen)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
return InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (outsideClickListenerActive)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement);
|
||||||
|
}
|
||||||
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dotNetReference?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private string GetInputTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-input" : $"{TestId}-input";
|
private string GetInputTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-input" : $"{TestId}-input";
|
||||||
|
|
||||||
private string GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover";
|
private string GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
services:
|
services:
|
||||||
|
worktracker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
playwright:
|
playwright:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
|
||||||
43
tests/playwright/calendar-pickers.spec.ts
Normal file
43
tests/playwright/calendar-pickers.spec.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { expect, test, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
async function waitForBlazorConnection(page: Page) {
|
||||||
|
await page.waitForTimeout(750);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('calendar modal saves and deletes an event with picker ranges', async ({ page }) => {
|
||||||
|
const eventTitle = `Smoke ${Date.now()}`;
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1440, height: 960 });
|
||||||
|
await page.goto('/calendar-event/2026-04-22', { waitUntil: 'networkidle' });
|
||||||
|
await waitForBlazorConnection(page);
|
||||||
|
|
||||||
|
const startDateInput = page.getByTestId('calendar-event-start-date-input');
|
||||||
|
const endDateInput = page.getByTestId('calendar-event-end-date-input');
|
||||||
|
await expect(startDateInput).toBeVisible();
|
||||||
|
await expect(endDateInput).toBeVisible();
|
||||||
|
|
||||||
|
await endDateInput.click();
|
||||||
|
await page.getByTestId('calendar-event-end-date-day-2026-04-23').click();
|
||||||
|
await expect(endDateInput).toHaveValue('23/04/2026');
|
||||||
|
|
||||||
|
const editorTimeInputs = page.locator('input[type="time"]');
|
||||||
|
await expect(editorTimeInputs).toHaveCount(2);
|
||||||
|
await editorTimeInputs.nth(0).fill('09:00');
|
||||||
|
await editorTimeInputs.nth(1).fill('12:00');
|
||||||
|
|
||||||
|
await page.locator('input[placeholder="Optional"]').fill(eventTitle);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
|
await waitForBlazorConnection(page);
|
||||||
|
await expect(page).toHaveURL(/\/calendar\/2026-04$/);
|
||||||
|
|
||||||
|
const savedEventEntries = page.locator('.calendar-item-event', { hasText: eventTitle });
|
||||||
|
await expect(savedEventEntries).toHaveCount(2);
|
||||||
|
await expect(savedEventEntries.first()).toContainText('09:00');
|
||||||
|
|
||||||
|
await savedEventEntries.first().click();
|
||||||
|
page.once('dialog', dialog => dialog.accept());
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText(eventTitle)).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test, type Page } from '@playwright/test';
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
locale: 'en-US'
|
locale: 'en-US'
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForBlazorConnection(page: Parameters<typeof test>[0]['page']) {
|
async function waitForBlazorConnection(page: Page) {
|
||||||
await page.waitForTimeout(750);
|
await page.waitForTimeout(750);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,8 +23,8 @@ test('date picker popup is monday-first and uses european formatting', async ({
|
||||||
|
|
||||||
const weekdayHeaders = popup.getByTestId('date-picker-weekday');
|
const weekdayHeaders = popup.getByTestId('date-picker-weekday');
|
||||||
await expect(weekdayHeaders).toHaveCount(7);
|
await expect(weekdayHeaders).toHaveCount(7);
|
||||||
await expect(weekdayHeaders.first()).toHaveText(/^(Mon|Lun)$/);
|
|
||||||
|
|
||||||
|
await expect(weekdayHeaders.first()).toHaveText(/^(Mon|Lun)$/);
|
||||||
await page.getByTestId('calendar-event-start-date-day-2026-04-22').click();
|
await page.getByTestId('calendar-event-start-date-day-2026-04-22').click();
|
||||||
await expect(startDateInput).toHaveValue(/^22\/\d{2}\/\d{4}$/);
|
await expect(startDateInput).toHaveValue(/^22\/\d{2}\/\d{4}$/);
|
||||||
});
|
});
|
||||||
|
|
@ -86,4 +86,42 @@ window.workTrackerPreferences = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.workTrackerDateInput = (() => {
|
||||||
|
const listeners = new WeakMap();
|
||||||
|
|
||||||
|
function unregisterOutsideClick(root) {
|
||||||
|
const existingListener = listeners.get(root);
|
||||||
|
if (!existingListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener("pointerdown", existingListener, true);
|
||||||
|
listeners.delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerOutsideClick(root, dotNetReference) {
|
||||||
|
if (!root || !dotNetReference) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterOutsideClick(root);
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
if (root.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dotNetReference.invokeMethodAsync("ClosePopupFromOutsideClickAsync");
|
||||||
|
};
|
||||||
|
|
||||||
|
listeners.set(root, listener);
|
||||||
|
document.addEventListener("pointerdown", listener, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerOutsideClick,
|
||||||
|
unregisterOutsideClick
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
window.workTrackerTheme.init();
|
window.workTrackerTheme.init();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue