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": [],
|
||||
"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",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "WorkTracker: Docker Debug Prepare",
|
||||
|
|
@ -38,58 +119,19 @@
|
|||
},
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithEdge",
|
||||
"pattern": "Now listening on:\\s+https?://\\S+:(\\d+)",
|
||||
"uriFormat": "http://localhost:8002/?ready=%s"
|
||||
"pattern": "\\bNow listening on:\\s+https?://\\S+",
|
||||
"uriFormat": "http://localhost:8002",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
"justMyCode": true,
|
||||
"requireExactSource": false,
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "WorkTracker: Debug App in Docker",
|
||||
"type": "coreclr",
|
||||
"name": "WorkTracker: Launch Integrated Browser for Running Docker App",
|
||||
"type": "editor-browser",
|
||||
"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 Edge",
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:8002",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
"url": "http://localhost:8002"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -197,6 +197,8 @@ else
|
|||
{
|
||||
selectedDate = value.Value;
|
||||
statusMessage = null;
|
||||
selectedDay = await WorkDayService.GetAsync(selectedDate);
|
||||
RecomputePreview();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
@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">
|
||||
<input id="@InputId"
|
||||
data-testid="@GetInputTestId()"
|
||||
|
|
@ -75,8 +77,28 @@
|
|||
private DateOnly? lastValue;
|
||||
private string displayValue = string.Empty;
|
||||
private bool isOpen;
|
||||
private bool outsideClickListenerActive;
|
||||
private DateOnly visibleMonth;
|
||||
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()
|
||||
{
|
||||
|
|
@ -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 GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
services:
|
||||
worktracker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
playwright:
|
||||
build:
|
||||
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({
|
||||
locale: 'en-US'
|
||||
});
|
||||
|
||||
async function waitForBlazorConnection(page: Parameters<typeof test>[0]['page']) {
|
||||
async function waitForBlazorConnection(page: Page) {
|
||||
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');
|
||||
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 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue