Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions src/Components/Agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Working on Issues in the Components Area

This guide provides step-by-step instructions for working on issues in the ASP.NET Core Components area.

## Working on New Features

### Overview

The workflow for implementing new features in the Components area follows these steps:

1. **Create a sample scenario first** - This is the most important first step. Update code in one of the projects in `src/Components/Samples` folder to include the scenarios for the feature you want to build. This allows you to develop and test the feature interactively before writing formal tests.

2. **Build and test interactively** - Build the feature and use Playwright to test it in the browser, ensuring it works end-to-end at a basic level.

### Sample Projects

The `src/Components/Samples` folder contains several sample projects you can use for developing and testing features:

- **BlazorServerApp** - A Blazor Server application for testing server-side scenarios
- **BlazorUnitedApp** - A Blazor Web App (united/hybrid mode) for testing combined server and WebAssembly scenarios
- **BlazorUnitedApp.Client** - The client-side portion of the BlazorUnitedApp

**Always start by adding your feature scenario to one of these sample projects first.** This allows you to:
- Quickly iterate on the implementation
- Test the feature interactively in a real browser
- Verify the feature works before writing formal E2E tests
- Debug issues more easily with full logging capabilities

3. **Debug when needed**:
- If something isn't working as expected, increase the logging level in the sample for `Microsoft.AspNetCore.Components` to `Debug` to see detailed logs.
- Check browser console logs using Playwright's `browser_console_messages`.
- Use Microsoft documentation to learn more about troubleshooting Blazor applications.
- You can also increase the log level for JavaScript console output.

4. **Validate the sample works** - You must have a validated, working sample in the Samples folder before proceeding. Use Playwright to confirm the feature works end-to-end in the browser.

5. **Implement E2E tests** - Only after the sample is validated, implement E2E tests for it.

6. **Clean up sample code** - After your E2E tests are passing, remove the sample code you added to the Samples projects. The sample was only for development and interactive testing; the E2E tests now provide the permanent test coverage.

## Build Tips

### Efficient Build Strategy

To avoid unnecessary full repository builds, follow this optimized approach:

#### 1. Initial Setup - Check for First Build
Before running any commands, check if a full build has already been completed:
- Look for `artifacts\agent-sentinel.txt` in the repository root
- If this file exists, skip to step 2
- If not present, run the initial build and create the sentinel file:

```bash
.\eng\build.cmd
echo "We ran eng\build.cmd successfully" > artifacts\agent-sentinel.txt
```

#### 2. Check for JavaScript Assets
Before running tests or samples, verify that JavaScript assets are built:
- Check for `src\Components\Web.JS\dist\Debug\blazor.web.js`
- If not present, run from the repository root: `npm run build`

#### 3. Iterating on C# Changes

**Most of the time (no dependency changes):**
```bash
dotnet build --no-restore -v:q
```

Or with `eng\build.cmd`:
```bash
.\eng\build.cmd -NoRestore -NoBuildDeps -NoBuildRepoTasks -NoBuildNative -NoBuildNodeJS -NoBuildJava -NoBuildInstallers -verbosity:quiet
```

**When you've added/changed project references or package dependencies:**

First restore:
```bash
.\restore.cmd
```

Then build:
```bash
dotnet build --no-restore -v:q
```

**Note:** The `-v:q` (or `-verbosity:quiet`) flag minimizes build output to only show success/failure and error details. Remove this flag if you need to see detailed build output for debugging.

#### 4. Building Individual Projects (Fixing Build Errors)

When fixing build errors in a specific project, you can build just that project without its dependencies for even faster iteration:

```bash
dotnet build <path-to-project.csproj> --no-restore --no-dependencies -v:q
```

**When to use `--no-dependencies`:**
- Fixing compilation errors in a single project (syntax errors, type errors, etc.)
- Making isolated changes that don't affect project references
- Rapid iteration on a specific library

**When NOT to use `--no-dependencies`:**
- You've changed public APIs that other projects depend on
- You need to verify that dependent projects still compile correctly
- You're unsure if your changes affect other projects (safer to build without this flag)

**Example:**
```bash
# Fix a compilation error in Components.Endpoints
dotnet build src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj --no-restore --no-dependencies -v:q
```

#### Quick Reference

1. **First time only**: `.\eng\build.cmd` → create `artifacts\agent-sentinel.txt`
2. **Check JS assets**: Verify `src\Components\Web.JS\dist\Debug\blazor.web.js` exists, run `npm run build` if missing
3. **Most C# changes**: `dotnet build --no-restore -v:q`
4. **Fixing build errors in one project**: `dotnet build <project.csproj> --no-restore --no-dependencies -v:q`
5. **Added/changed dependencies**: Run `.\restore.cmd` first, then use step 3

### E2E Testing Structure

Tests live in `src/Components/test`. The structure includes:

- **TestAssets folder** - Contains test assets and scenarios
- **Components.TestServer project** - A web application that launches multiple web servers with different scenarios (different project startups). Avoid adding new startup files unless strictly necessary.

### Running E2E Tests Manually

1. **Build the repository** first with `.\eng\build.cmd` from the repo root
2. **Start Components.TestServer**:
```bash
cd src\Components\test\testassets\Components.TestServer
dotnet run --project Components.TestServer.csproj
```
3. **Navigate to the test server** - The main server runs on `http://127.0.0.1:5019/subdir`
4. **Select a test scenario** - The main page shows a dropdown with all available test components
5. **Reproduce the scenario** to verify it works the same way as in the sample

Note: There are also other server instances launched for different test configurations (authentication, CORS, prerendering, etc.). These are listed in the "scenarios" table on the main page.

### Understanding Logging Configuration

#### Server-side (.NET) Logging

The server uses `Microsoft.Extensions.Logging.Testing.TestSink` for capturing logs. Log configuration is in `Program.cs`:

```csharp
.ConfigureLogging((ctx, lb) =>
{
TestSink sink = new TestSink();
lb.AddProvider(new TestLoggerProvider(sink));
lb.Services.Add(ServiceDescriptor.Singleton(sink));
})
```

#### Client-side (Blazor WebAssembly) Logging

Logs appear in the browser console. Log levels:
- Logs with `warn:` prefix are Warning level
- Logs with `info:` prefix are Information level
- Logs with `fail:` prefix are Error level

The Blazor WebAssembly log level can be configured at startup:

```javascript
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
```

LogLevel values: Trace=0, Debug=1, Information=2, Warning=3, Error=4, Critical=5

For Server-side Blazor (SignalR):
```javascript
Blazor.start({
circuit: {
configureSignalR: builder => {
builder.configureLogging("debug") // LogLevel.Debug
}
}
});
```

#### Viewing Logs in Playwright

Use `browser_console_messages` to see JavaScript console output including .NET logs routed to the console.

### Creating E2E Tests

E2E tests are located in `src/Components/test/E2ETest`.

1. First, check if there are already E2E tests for the component/feature area you're working on
2. Try to add an additional test to existing test files when possible
3. When adding test coverage, prefer extending existing test components and assets over creating a set of new ones if it doesn't complicate the existing ones excessively. This reduces test infrastructure complexity and keeps related scenarios together.

### Running E2E Tests

The E2E tests use Selenium. To build and run tests:

```bash
# Build the E2E test project (this includes all test assets as dependencies)
dotnet build src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-restore -v:q

# Run a specific test
dotnet test src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-build --filter "FullyQualifiedName~TestName"
```

**Important**: Never run all E2E tests locally as that is extremely costly. Full test runs should only happen on CI machines.

If a test is failing, it's best to run the server manually and navigate to the test to investigate. The test output won't be very useful for debugging.

Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ function tryApplyValueProperty(element: Element, value: string | null): boolean
return true;
}
default:
// Support contenteditable elements - set textContent as the value
if (element instanceof HTMLElement && element.isContentEditable) {
element.textContent = value || '';
return true;
}
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ function getFormFieldData(elem: Element) {
return { value: elem.value };
}

// Support contenteditable elements - use textContent as the value
if (elem instanceof HTMLElement && elem.isContentEditable) {
return { value: elem.textContent || '' };
}

return null;
}
7 changes: 7 additions & 0 deletions src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ function parseChangeEvent(event: Event): ChangeEventArgs {
.filter(option => option.selected)
.map(option => option.value);
return { value: selectedValues };
} else if (isContentEditableElement(element)) {
// For contenteditable elements, use textContent as the value
return { value: (element as HTMLElement).textContent || '' };
} else {
const targetIsCheckbox = isCheckbox(element);
const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
Expand Down Expand Up @@ -322,6 +325,10 @@ function isCheckbox(element: Element | null): boolean {
return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}

function isContentEditableElement(element: Element | null): element is HTMLElement {
return !!element && element instanceof HTMLElement && element.isContentEditable;
}

const timeBasedInputs = [
'date',
'datetime-local',
Expand Down
57 changes: 57 additions & 0 deletions src/Components/test/E2ETest/Tests/BindTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2093,4 +2093,61 @@ private void ApplyInputValue(string cssSelector, string value)
javascript.ExecuteScript(
$"document.querySelector('{cssSelector}').dispatchEvent(new KeyboardEvent('change'));");
}

[Fact]
public void CanBindContentEditableDiv_WithOninput()
{
var target = Browser.Exists(By.Id("contenteditable-oninput"));
var boundValue = Browser.Exists(By.Id("contenteditable-oninput-value"));
Assert.Equal("Initial content", target.Text);
Assert.Equal("Initial content", boundValue.Text);

// Use JavaScript to set content and dispatch input event
// (SendKeys on contenteditable causes issues because Blazor re-rendering moves the caret)
((IJavaScriptExecutor)Browser).ExecuteScript(@"
var el = arguments[0];
el.textContent = 'New content';
el.dispatchEvent(new Event('input', { bubbles: true }));
", target);

Browser.Equal("New content", () => boundValue.Text);
}

[Fact]
public void CanBindContentEditableDiv_WithOnchange()
{
var target = Browser.Exists(By.Id("contenteditable-onchange"));
var boundValue = Browser.Exists(By.Id("contenteditable-onchange-value"));
Assert.Equal("Change on blur", target.Text);
Assert.Equal("Change on blur", boundValue.Text);

// Focus the element first, then set content, then blur
// The contentblur event fires on blur, so we need proper focus/blur sequence
((IJavaScriptExecutor)Browser).ExecuteScript(@"
var el = arguments[0];
el.focus();
el.textContent = 'Updated value';
el.blur();
", target);

Browser.Equal("Updated value", () => boundValue.Text);
}

[Fact]
public void CanBindContentEditableDiv_InitiallyBlank()
{
var target = Browser.Exists(By.Id("contenteditable-initially-blank"));
var boundValue = Browser.Exists(By.Id("contenteditable-initially-blank-value"));
Assert.Equal(string.Empty, target.Text);
Assert.Equal(string.Empty, boundValue.Text);

// Use JavaScript to set content and dispatch input event
((IJavaScriptExecutor)Browser).ExecuteScript(@"
var el = arguments[0];
el.textContent = 'Added content';
el.dispatchEvent(new Event('input', { bubbles: true }));
", target);

Browser.Equal("Added content", () => boundValue.Text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,32 @@
<span id="time-default-step-textbox-timeonly-value">@timeStepTextboxTimeOnlyValue.ToLongTimeString()</span>
</p>

<h2>ContentEditable</h2>
<p>
ContentEditable div with oninput (standard event):
<div id="contenteditable-oninput"
contenteditable="true"
style="border: 1px solid #ccc; padding: 5px; min-height: 30px;"
@oninput="OnContentEditableInput">@contentEditableValue</div>
<span id="contenteditable-oninput-value">@contentEditableValue</span>
</p>
<p>
ContentEditable div with custom contentblur event (blur doesn't fire change on contenteditable):
<div id="contenteditable-onchange"
contenteditable="true"
style="border: 1px solid #ccc; padding: 5px; min-height: 30px;"
@oncontentblur="OnContentEditableChange">@contentEditableChangeValue</div>
<span id="contenteditable-onchange-value">@contentEditableChangeValue</span>
</p>
<p>
ContentEditable initially blank:
<div id="contenteditable-initially-blank"
contenteditable="true"
style="border: 1px solid #ccc; padding: 5px; min-height: 30px;"
@oninput="OnContentEditableBlankInput">@contentEditableBlankValue</div>
<span id="contenteditable-initially-blank-value">@contentEditableBlankValue</span>
</p>

@code {
string textboxInitiallyBlankValue = null;
string textboxInitiallyPopulatedValue = "Hello";
Expand Down Expand Up @@ -575,4 +601,24 @@
selectValue = SelectableValue.Sixth;
variableValue = SelectableValue.Sixth;
}

// ContentEditable binding
string contentEditableValue = "Initial content";
string contentEditableChangeValue = "Change on blur";
string contentEditableBlankValue = "";

void OnContentEditableInput(ChangeEventArgs e)
{
contentEditableValue = e.Value?.ToString() ?? "";
}

void OnContentEditableChange(ContentEditableEventArgs e)
{
contentEditableChangeValue = e.TextContent ?? "";
}

void OnContentEditableBlankInput(ChangeEventArgs e)
{
contentEditableBlankValue = e.Value?.ToString() ?? "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace BasicTestApp.CustomEventTypesNamespace;
[EventHandler("onkeydown.yetanother", typeof(YetAnotherCustomKeyboardEventArgs), true, true)]
[EventHandler("oncustommouseover", typeof(EventArgs), true, true)]
[EventHandler("onsendjsobject", typeof(EventWithCustomSerializedDataEventArgs), true, true)]
[EventHandler("oncontentblur", typeof(ContentEditableEventArgs), true, true)]
public static class EventHandlers
{
}
Expand Down Expand Up @@ -48,3 +49,12 @@ public DotNetType(string propertyValue)
Property = propertyValue;
}
}

/// <summary>
/// Event arguments for contenteditable elements that capture both textContent and innerHTML.
/// </summary>
class ContentEditableEventArgs : EventArgs
{
public string TextContent { get; set; }
public string InnerHTML { get; set; }
}
Loading
Loading