GuyStore - Test Suite Build Details

Complete record of the test suite added to Stage 1  ·  Built 2026-04-29

Table of Contents

  1. Overview & Final Results
  2. Solution Structure
  3. Unit Test Project
  4. Integration Test Project
  5. E2E Test Project
  6. Changes to the Main GuyStore App
  7. Bugs Found and Fixed
  8. Voice Feature
  9. How to Run

Overview & Final Results

Three test projects were added alongside the existing GuyStore app. All tests run entirely in-memory - no SQL Server, no running app, no external services required. Total: 97 passing tests across unit, integration, and end-to-end layers.

33
Unit tests
43
Integration tests
21
E2E tests
97
Total · All pass

Per-project result (last run)

UNIT GuyStore.Tests.Unit.dll 33 / 33 ~824 ms
INT GuyStore.Tests.Integration.dll 43 / 43 ~5 s
E2E GuyStore.Tests.E2E.dll 21 / 21 ~44 s

Solution Structure

C:\Guy\Notes\Claud Projects\Guy\
│
├── GuyStore\                          ← main ASP.NET Core 9 MVC app
│   ├── GuyStore.csproj
│   ├── Program.cs                     ← modified: Testing env + InMemory DB + partial Program
│   ├── Data\DataSeeder.cs
│   ├── Controllers\
│   ├── Areas\Admin\
│   ├── Models\
│   ├── Services\CartService.cs
│   ├── Views\
│   └── wwwroot\
│
├── GuyStore.Tests.Unit\               ← xUnit · Moq · no HTTP
│   ├── GuyStore.Tests.Unit.csproj
│   ├── GlobalUsings.cs
│   ├── Helpers\FakeSession.cs
│   ├── Services\CartServiceTests.cs
│   ├── Models\CartItemTests.cs
│   ├── Models\ApplicationUserTests.cs
│   └── Controllers\NotificationsControllerTests.cs
│
├── GuyStore.Tests.Integration\        ← xUnit · WebApplicationFactory · TestServer
│   ├── GuyStore.Tests.Integration.csproj
│   ├── GlobalUsings.cs
│   ├── Helpers\GuyStoreWebApplicationFactory.cs
│   ├── Helpers\AuthHelper.cs
│   └── Controllers\
│       ├── AccountControllerTests.cs
│       ├── ShopControllerTests.cs
│       ├── ThemeControllerTests.cs
│       ├── CartControllerTests.cs
│       ├── AdminProductsControllerTests.cs
│       ├── AdminOrdersControllerTests.cs
│       └── AdminAccountsControllerTests.cs
│
├── GuyStore.Tests.E2E\                ← xUnit · Microsoft.Playwright · real Kestrel
│   ├── GuyStore.Tests.E2E.csproj
│   ├── GlobalUsings.cs
│   ├── Fixtures\PlaywrightFixture.cs
│   └── Tests\
│       ├── LoginTests.cs
│       ├── ShopTests.cs
│       ├── CheckoutTests.cs
│       ├── AdminTests.cs
│       └── ThemeTests.cs
│
└── GuyStore.sln                       ← all four projects

Unit Test Project - GuyStore.Tests.Unit

Packages

PackageVersionPurpose
xunit2.9.2Test framework
xunit.runner.visualstudio2.8.2VS / dotnet test runner
Microsoft.NET.Test.Sdk17.12.0Test SDK
Moq4.20.72Mocking framework
Microsoft.AspNetCore.App (FrameworkReference)-Access to ASP.NET Core types

Project reference

References GuyStore\GuyStore.csproj directly - no mocking of the app layer needed for pure logic tests.

Key files created

GlobalUsings.cs

global using Xunit;

Eliminates the need for using Xunit; in every test file.

Helpers/FakeSession.cs

A Dictionary<string, byte[]>-backed implementation of ISession, enabling CartService to be tested without a real HTTP context or middleware pipeline.

Services/CartServiceTests.cs - 13 tests

TestWhat it verifies
GetCart_EmptySessionReturns empty list when session is blank
AddItem_NewItemFirst add creates entry with correct quantity
AddItem_SameProduct_AccumulatesQuantitySecond add to same product increments quantity
AddItem_DifferentProductsTwo adds produce two separate cart lines
UpdateQuantity_PositiveSets quantity to new value
UpdateQuantity_ZeroZero quantity removes the item
UpdateQuantity_NegativeNegative quantity removes the item
RemoveItem_ExistingRemoves correct line, leaves others
RemoveItem_NonExistentNo exception if productId not in cart
ClearCartEmpties the entire cart
ItemCountSum of all line quantities
TotalSum of Price × Quantity for all lines
Total_EmptyReturns 0m for empty cart

Models/CartItemTests.cs - 2 tests

Models/ApplicationUserTests.cs - 3 tests

Controllers/NotificationsControllerTests.cs - 7 tests

Integration Test Project - GuyStore.Tests.Integration

Packages

PackageVersionPurpose
xunit2.9.2Test framework
xunit.runner.visualstudio2.8.2Runner
Microsoft.NET.Test.Sdk17.12.0Test SDK
Microsoft.AspNetCore.Mvc.Testing9.0.4WebApplicationFactory + TestServer
Microsoft.EntityFrameworkCore.InMemory9.0.4In-memory DB provider for tests

Helpers/GuyStoreWebApplicationFactory.cs

public class GuyStoreWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");
    }
}

Setting the environment to Testing is the only override needed. Program.cs detects this and registers the EF Core InMemory provider instead of SQL Server - and seeds the test data - automatically.

Helpers/AuthHelper.cs

Three static methods used across all integration test classes:

Test pattern - FreshClient()

Each test calls FreshClient() (creates a new HttpClient from the factory) rather than sharing a single client. This prevents one test's auth cookie from leaking into the next.

Controllers tested (43 tests total)

FileTestsCoverage
AccountControllerTests.cs7 Login GET/POST valid/wrong-pw/pending, Register GET/POST, Profile unauthenticated redirect
ShopControllerTests.cs7 Index shows published, hides unpublished, search, category filter, detail page, image endpoint, 404 for missing product
ThemeControllerTests.cs3 Sets cookie for valid theme, defaults to corporate for unknown theme, rejects external returnUrl
CartControllerTests.cs7 Add item, view cart, update qty, remove item, checkout page GET, place order POST, unauthenticated redirect
AdminProductsControllerTests.cs7 List, Create GET/POST, Edit GET/POST, Delete, non-admin → 403
AdminOrdersControllerTests.cs5 Order list, order detail, update status (pending/shipped/delivered)
AdminAccountsControllerTests.cs6 Account list, approve user, reject user, AccountManagement role access, cross-role restriction

E2E Test Project - GuyStore.Tests.E2E

Packages

PackageVersionPurpose
xunit2.9.2Test framework
xunit.runner.visualstudio2.8.2Runner
Microsoft.NET.Test.Sdk17.12.0Test SDK
Microsoft.Playwright1.49.0Browser automation (Chromium)
Microsoft.EntityFrameworkCore.InMemory9.0.4In-memory DB for the embedded server

Fixtures/PlaywrightFixture.cs

Shared via [Collection("E2E")] - one instance starts the server and browser before the first test, then tears them down after the last.

// WebApplicationOptions that make this work:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    ApplicationName = "GuyStore",          // ← critical: loads compiled Razor views from GuyStore.dll
    ContentRootPath = guyStoreRoot,        // ← found by walking up to GuyStore.sln
    WebRootPath    = Path.Combine(guyStoreRoot, "wwwroot"),
    EnvironmentName = "Testing"
});

// Dynamic port - avoids "address already in use" between runs
_app.Urls.Add("http://127.0.0.1:0");
await _app.StartAsync();

var server = _app.Services.GetRequiredService<IServer>();
var feature = server.Features.Get<IServerAddressesFeature>();
BaseUrl = feature!.Addresses.First().TrimEnd('/');

The fixture also registers Identity, session, SignalR, CartService, and the InMemory DB; calls DataSeeder.SeedAsync; and creates a headless Chromium browser via Playwright.CreateAsync().

Tests (21 total)

FileTestsScenarios
LoginTests.cs4 Admin login succeeds, wrong password shows error, pending account shows message, logout redirects home
ShopTests.cs5 Published products visible, unpublished hidden, search filter, category filter, detail page loads
CheckoutTests.cs4 Add to cart, full checkout flow (PO number → confirmation), unauthenticated cart → login redirect, My Orders page
AdminTests.cs5 Dashboard stats, all products (incl. drafts), send notification, AccountManagement can access accounts, cannot access products
ThemeTests.cs3 Switch to dark sets cookie, switch to corporate sets cookie, theme persists across navigation

Changes to the Main GuyStore App

GuyStore.csproj - added package

<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />

Required so integration and E2E tests can substitute the InMemory provider for SQL Server at runtime.

Program.cs - three changes

1. Environment-aware DB registration

if (builder.Environment.IsEnvironment("Testing"))
{
    var testDbName = "GuyStoreTestDb_" + Guid.NewGuid();   // unique per run
    builder.Services.AddDbContext<ApplicationDbContext>(opt =>
        opt.UseInMemoryDatabase(testDbName));
}
else
{
    builder.Services.AddDbContext<ApplicationDbContext>(opt =>
        opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
}

The Guid is captured outside the lambda. If it were inside, EF Core would generate a new DB name per DbContext instance and products seeded in one context would be invisible to another.

2. Environment-aware migration vs EnsureCreated

if (app.Environment.IsEnvironment("Testing"))
    await db.Database.EnsureCreatedAsync();   // InMemory has no migrations
else
    await db.Database.MigrateAsync();

3. Partial Program class for WebApplicationFactory

// bottom of Program.cs
public partial class Program { }

WebApplicationFactory<Program> needs a public or internal reference to Program. The minimal-API entry-point is a compiler-generated class with limited accessibility; the partial declaration promotes it.

Bugs Found and Fixed During the Build

All bugs below were discovered while getting the tests to pass - not pre-existing issues logged elsewhere.

BUG 1 - xUnit Fact/Theory attributes not found (all three projects)

Symptom: Every test file reported "The type or namespace name 'Fact' could not be found."

Root cause: No using Xunit; in any test file.

Fix: Added GlobalUsings.cs with global using Xunit; to each project - one file eliminates the import from every test class.

BUG 2 - EF Core dual-provider conflict ("services for 'SqlServer' and 'InMemory' both registered")

Symptom: Integration tests threw InvalidOperationException on first DB access.

Root cause: The WebApplicationFactory attempted to remove only the SQL Server DbContextOptions descriptor and add InMemory, but EF Core 9 still saw both providers registered.

Fix: Made Program.cs fully environment-aware (see above). The factory only needs builder.UseEnvironment("Testing") - no patching of ConfigureServices required.

BUG 3 - InMemory DB differs per request (seeded products not visible in shop)

Symptom: Integration tests for the shop index returned an empty product list even though seeding succeeded.

Root cause: Guid.NewGuid() was called inside the AddDbContext lambda, so every new DbContext instance received a different database name - seeded data was in DB "X" while the controller queried DB "Y".

Fix: Captured the name once outside the lambda: var testDbName = "GuyStoreTestDb_" + Guid.NewGuid(); … opt.UseInMemoryDatabase(testDbName);

BUG 4 - Shared HttpClient auth state between integration tests

Symptom: Test ordering mattered - if test A logged in, test B (which expected to be unauthenticated) saw an auth cookie.

Root cause: All tests in a class shared one _client field; cookies accumulated across tests.

Fix: Each test calls FreshClient() which creates a new HttpClient from the factory per test, with an isolated cookie jar.

BUG 5 - E2E server port conflict ("address already in use")

Symptom: E2E tests failed intermittently when re-run shortly after a previous run - the previous Kestrel instance hadn't fully released port 5099.

Root cause: PlaywrightFixture had a hardcoded port.

Fix: Changed to http://127.0.0.1:0 (OS picks a free port) and read the actual port back via IServerAddressesFeature.

BUG 6 - Playwright pages showed blank/error content (all E2E tests timing out)

Symptom: Every E2E test timed out with "waiting for Locator(input[name='Email'])". The server started, but no page rendered the expected HTML.

Root cause: PlaywrightFixture creates a new WebApplication inside the test process. Without ApplicationName = "GuyStore" in WebApplicationOptions, ASP.NET Core's MVC view engine used the test assembly name (GuyStore.Tests.E2E) to locate precompiled Razor views. It couldn't find them in GuyStore.dll, so every route rendered an error page.

Fix: Added ApplicationName = "GuyStore" to WebApplicationOptions. This tells the view engine to look for compiled views in GuyStore.dll, not the test DLL.

BUG 7 - Submit button found but "not visible" (login/checkout E2E tests)

Symptom: After the ApplicationName fix, tests progressed but timed out trying to click button[type='submit']. Playwright reported the element "not visible".

Root cause: The layout's navbar contains Bootstrap dropdown menus for the theme switcher (and logout). These menus include button[type='submit'] elements that appear before the login/checkout buttons in DOM order. Bootstrap dropdowns are hidden by default (display: none), so page.ClickAsync("button[type='submit']") found the hidden dropdown button first and tried (and failed) to click it.

Fix: Changed all login/checkout button selectors to button.btn-primary[type='submit']. The main action buttons use the Bootstrap btn-primary class; the dropdown theme/logout buttons use dropdown-item, so this selector is unambiguous.

BUG 8 - "Add to Cart" assertion fails for unauthenticated users (E2E shop detail)

Symptom: Shop_ProductDetail_ShowsProductInfo asserted Assert.Contains("Add to Cart", html) but the page showed "Login to Order" instead.

Root cause: The Shop Detail view conditionally renders Add to Cart only when the user is authenticated. Unauthenticated visitors see Login to Order. The test navigated as an unauthenticated user.

Fix: Assert.True(html.Contains("Add to Cart") || html.Contains("Login to Order"))

BUG 9 - Theme switch tests: hidden dropdown elements and GET vs POST mismatch

Symptom: SwitchToDark timed out trying to click a hidden <input type="hidden" name="theme" value="dark">. SwitchToCorporate failed with cookie null because page.GotoAsync("/Theme/Switch?theme=corporate") issued a GET, but the controller action is [HttpPost] only (405 returned, no cookie set).

Root cause: Both theme switch tests tried to interact with elements inside Bootstrap dropdowns (hidden by default). The locator input[name='theme'][value='dark'] matched the hidden field, and CountAsync() > 0 returned true - so the test tried to click the hidden input rather than falling through to the else branch.

Fix: Both tests now submit the theme form directly via JavaScript - bypassing the hidden dropdown - and use RunAndWaitForNavigationAsync to correctly wait for the POST → redirect → page-load sequence before reading cookies.

await page.RunAndWaitForNavigationAsync(async () =>
    await page.EvaluateAsync(
        "document.querySelector(\"input[name='theme'][value='dark']\").closest('form').submit()"),
    new PageRunAndWaitForNavigationOptions { WaitUntil = WaitUntilState.NetworkIdle });
BUG 10 - SwitchToDark: race condition between form.submit() and WaitForLoadStateAsync

Symptom: After switching to the JS form-submit approach, SwitchToCorporate passed but SwitchToDark failed with Assert.NotNull() - cookie is null even in isolation (no prior tests interfering).

Root cause: form.submit() triggers a navigation asynchronously. Playwright's EvaluateAsync returns as soon as the JavaScript expression returns (i.e. immediately after calling submit(), not after the navigation completes). If the subsequent WaitForLoadStateAsync(NetworkIdle) fires before the navigation has started, it sees the page as already idle and returns immediately - before the POST response and its Set-Cookie header have been processed.

Why corporate didn't hit this race: Corporate test runs first in a fresh context; the timing happened to be forgiving. Dark runs second (or in isolation) where the race consistently fires.

Fix: RunAndWaitForNavigationAsync starts listening for a navigation event before the trigger fires, so it reliably waits for the full POST → 302 → GET cycle to complete before the test reads cookies.

BUG 11 - Logout button: hidden inside Bootstrap dropdown

Symptom: Logout_AuthenticatedUser_RedirectsToHome timed out - Playwright found the logout button in the user dropdown but it was invisible (display: none on the closed dropdown menu).

Root cause: The Sign Out button is inside a .dropdown-menu that is only visible when the user clicks the dropdown toggle.

Fix: Bypass visibility by submitting the logout form via JavaScript. The form already contains the rendered __RequestVerificationToken hidden field, so the anti-forgery check passes.

await page.EvaluateAsync("document.querySelector(\"form[action*='Logout']\").submit()");
BUG 13 - Voice Camera: InvalidStateError when restarting SpeechRecognition after photo capture

File: wwwroot/js/voice-camera.jsrecognition.onend handler and startVoice() function.

Symptom: After taking a photo via voice command, the browser console reported "InvalidStateError: recognition has already started" on the keep-alive restart attempt.

Root cause: When a photo was captured, capturePhoto() called stopVoice(), which set isListening = false and called recognition.stop(). However, iOS Safari (and some desktop browsers) also fires onend independently when recognition auto-stops after detecting a result. If onend fired while isListening was still true (race condition between the auto-stop and the manual stop from the capture), the handler called recognition.start() on the same object while its internal state machine was still in the "ending" phase — not yet idle — throwing InvalidStateError. Even without the race, calling start() immediately inside onend is inherently unsafe because the browser has not fully torn down the session at the point onend fires.

Fix — two-part change:

1. recognition.onend: Replaced the immediate recognition.start() call with a setTimeout(startVoice, 150). The 150 ms gap lets the browser fully close the previous session before a new one is opened. An early-return guard (if (!isListening) return) ensures no restart happens after a photo capture has already set isListening = false.

// Before (broken)
recognition.onend = () => {
    if (isListening) recognition.start(); // keep alive
};

// After (fixed)
recognition.onend = () => {
    if (!isListening) return;
    setTimeout(startVoice, 150); // fresh instance avoids InvalidStateError
};

2. startVoice(): Before creating a new SpeechRecognition instance, any existing instance has its onend handler nulled out first, then stopped. Nulling onend is the critical step — without it, calling recognition.stop() inside startVoice() would fire onendstartVoicestop() again, creating an infinite restart loop.

// Added at the top of startVoice(), before new SpeechRecognition()
if (recognition) { recognition.onend = null; recognition.stop(); recognition = null; }

Together these changes guarantee that recognition.start() is always called on a brand-new, never-started instance with no lingering state from a previous session.

BUG 12 - Razor tag helper: C# variable used as bare HTML attribute in <option>

File: Views/VoiceFeature/Translate.cshtml — both the source-language and target-language <select> loops.

Symptom: Build error: "The tag helper 'option' must not have C# in the element's attribute declaration area."

Root cause: A C# variable was placed directly in the attribute list of an <option> element — <option value="@l.Code" @selected> — which is not legal in a Razor view that has tag helpers active. The Razor parser treats option as a tag helper and rejects bare C# expressions inside the opening tag.

Fix: Use the selected attribute with a null-conditional expression. Razor omits the attribute entirely when the value is null, so no selected="" is rendered for non-matching options:

<!-- Before (broken) -->
@{
    var selected = l.Code == "zh-TW" ? "selected" : "";
}
<option value="@l.Code" @selected>@l.NativeName — @l.Name</option>

<!-- After (fixed) -->
<option value="@l.Code" selected="@(l.Code == "zh-TW" ? "selected" : null)">@l.NativeName — @l.Name</option>
BUG 14 — VS 2022 build warnings: QuestPDF version pin & Playwright obsolete API (2026-05-27)

Warning 1 — NU1603: QuestPDF version mismatch (all 4 projects)

Symptom: Every project in the solution emitted:

warning NU1603: GuyStore depends on QuestPDF (>= 2024.10.5) but QuestPDF 2024.10.5 was not found.
QuestPDF 2024.12.0 was resolved instead.

Root cause: GuyStore.csproj pinned QuestPDF to an exact minimum of 2024.10.5, but that version is no longer available on NuGet; the lowest available release in the 2024.x family is now 2024.12.0. NuGet resolved to the nearest available version but logged the mismatch as a warning. All three test projects inherited the same warning transitively.

Fix: Updated the version reference in GuyStore.csproj to match exactly what NuGet resolved:

<!-- Before -->
<PackageReference Include="QuestPDF" Version="2024.10.5" />

<!-- After -->
<PackageReference Include="QuestPDF" Version="2024.12.0" />

Warning 2 — CS0612: RunAndWaitForNavigationAsync is obsolete (20 call sites, 6 E2E test files)

Symptom: 20 CS0612 warnings across the E2E project:

warning CS0612: 'IPage.RunAndWaitForNavigationAsync(Func<Task>, PageRunAndWaitForNavigationOptions?)' is obsolete

Affected files: ReviewTests.cs (3), DiscountTests.cs (5), AddressTests.cs (3), Stage2Tests.cs (2), ThemeTests.cs (2), InvoiceTests.cs (3), Stage3Tests.cs (2).

Root cause: IPage.RunAndWaitForNavigationAsync was deprecated in Microsoft.Playwright 1.26+. The method still compiles and runs, but Playwright marks it [Obsolete] in favour of a simpler sequential pattern.

Fix: Replaced every call with the equivalent two-step sequence — trigger the action first, then wait for the page to reach NetworkIdle:

// Before (obsolete) — click + navigation bundled
await page.RunAndWaitForNavigationAsync(
    () => page.ClickAsync("button[type='submit']"),
    new PageRunAndWaitForNavigationOptions { WaitUntil = WaitUntilState.NetworkIdle });

// After — equivalent, no obsolete API
await page.ClickAsync("button[type='submit']");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

For the two EvaluateAsync (JS form-submit) variants in ThemeTests.cs and Stage2Tests.cs:

// Before
await page.RunAndWaitForNavigationAsync(async () =>
    await page.EvaluateAsync("document.querySelector(...).closest('form').submit()"),
    new PageRunAndWaitForNavigationOptions { WaitUntil = WaitUntilState.NetworkIdle });

// After
await page.EvaluateAsync("document.querySelector(...).closest('form').submit()");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

Result after both fixes:

Build succeeded.
    0 Warning(s)
    0 Error(s)

Voice Feature

Two browser-based voice tools added to the main navigation under a 🎤 Voice dropdown. Both use the Web Speech API for microphone input and require no backend infrastructure beyond the existing ASP.NET Core app.

New Files

PathTypePurpose
Controllers/VoiceFeatureController.csControllerCamera GET, Translate GET + POST, Languages GET
Services/ITranslationService.csInterfaceContract for TranslateAsync and GetSupportedLanguages
Services/TranslationService.csServiceMyMemory API integration (simulation mode or live)
Views/VoiceFeature/Camera.cshtmlViewLive camera feed + voice-triggered capture UI
Views/VoiceFeature/Translate.cshtmlViewLanguage-pair selector + voice input + output box
wwwroot/js/voice-camera.jsJSMediaDevices, SpeechRecognition, canvas capture, download
wwwroot/js/voice-translate.jsJSSpeechRecognition, POST to API, history, SpeechSynthesis

Modified Files

PathChange
Views/Shared/_Layout.cshtmlAdded Voice dropdown menu item in navbar
Program.csRegistered ITranslationService / TranslationService as scoped

1 — Voice Camera (/VoiceFeature/Camera)

Opens the device camera (rear by default, togglable to front) with a live video preview. Speak a trigger word and the page captures a still, shows it as a preview, and offers one-click download — no server round-trip needed.

Voice Triggers

LanguageRecognised phrases
Englishphoto, picture, snap, take photo, take picture, capture
Traditional Chinese拍照, 拍一張, 照相, 截圖, 拍攝
Simplified Chinese拍照, 拍一张, 照相, 截图, 拍摄

Key JS logic (voice-camera.js)

// SpeechRecognition fires interimResults continuously
recognition.onresult = e => {
    const transcript = Array.from(e.results)
        .map(r => r[0].transcript.toLowerCase()).join(' ');
    if (TRIGGERS[currentLang].some(t => transcript.includes(t)))
        capturePhoto();   // draws video frame to canvas → PNG data URL
};

Capture writes the video frame to an off-screen <canvas>, converts it to a PNG data URL, and assigns it to both an <img> preview and a download anchor. Camera toggle switches facingMode between environment and user.


2 — Voice Translate (/VoiceFeature/Translate)

Speaks into the source-language box, hits Translate, and reads the result aloud. Supports 12 languages with a one-click swap button.

Supported Languages (12)

CodeLanguageCodeLanguage
enEnglishfrFrench
zh-TWTraditional ChinesedeGerman
zh-CNSimplified ChineseesSpanish
jaJapaneseitItalian
koKoreanptPortuguese
viVietnamesethThai

Controller API endpoint

// POST /VoiceFeature/Translate
[HttpPost]
public async Task<IActionResult> Translate([FromBody] TranslateRequest req)
{
    var result = await _translation.TranslateAsync(req.Text, req.From, req.To);
    return Ok(new { translation = result });
}

Translation service backends

ModeSettingNotes
Simulation (default)Translation:SimulationMode = trueReturns mock text — zero dependencies
MyMemory APISimulationMode = false (no key needed)Free, 2 000 chars/day per IP
Azure TranslatorTranslation:AzureKey + AzureRegionFull throughput, requires Azure subscription

Frontend features (voice-translate.js)


Navigation menu addition (_Layout.cshtml)

<li class="nav-item dropdown">
    <a class="nav-link dropdown-toggle
       @(ViewContext.RouteData.Values["controller"]?.ToString() == "VoiceFeature" ? "active" : "")"
       href="#" role="button" data-bs-toggle="dropdown">🎤 Voice</a>
    <ul class="dropdown-menu">
        <li><a class="dropdown-item" asp-controller="VoiceFeature" asp-action="Camera">📷 Voice Camera</a></li>
        <li><a class="dropdown-item" asp-controller="VoiceFeature" asp-action="Translate">🌐 Voice Translate</a></li>
    </ul>
</li>

Service registration (Program.cs)

builder.Services.AddScoped<ITranslationService, TranslationService>();

Browser requirements

FeatureRequired APIsSupported browsers
Voice CameraMediaDevices, SpeechRecognition, CanvasChrome, Edge, Safari
Voice TranslateSpeechRecognition, SpeechSynthesisChrome, Edge (Firefox partial)

Both features require HTTPS or localhost for microphone access.

How to Run

Prerequisites

Step 1 - Build the solution

cd "C:\Guy\Notes\Claud Projects\Guy"
dotnet build GuyStore.sln

Step 2 - Install Playwright browsers (once per machine)

pwsh GuyStore.Tests.E2E\bin\Debug\net9.0\playwright.ps1 install chromium

On Windows PowerShell 5 use powershell instead of pwsh.

Step 3 - Run all tests

# All 97 tests at once
dotnet test GuyStore.sln

# Individual projects
dotnet test GuyStore.Tests.Unit\GuyStore.Tests.Unit.csproj
dotnet test GuyStore.Tests.Integration\GuyStore.Tests.Integration.csproj
dotnet test GuyStore.Tests.E2E\GuyStore.Tests.E2E.csproj

# Verbose output (shows each test name)
dotnet test GuyStore.sln -v normal

# Single test by name
dotnet test GuyStore.Tests.E2E\GuyStore.Tests.E2E.csproj --filter "FullCheckoutFlow_PlacesOrderSuccessfully"

# All tests in one class
dotnet test GuyStore.Tests.E2E\GuyStore.Tests.E2E.csproj --filter "ShopTests"

Expected output

Passed!  - Failed:  0, Passed: 33, Skipped: 0, Total: 33, Duration: ~800 ms  - GuyStore.Tests.Unit.dll
Passed!  - Failed:  0, Passed: 43, Skipped: 0, Total: 43, Duration: ~5 s    - GuyStore.Tests.Integration.dll
Passed!  - Failed:  0, Passed: 21, Skipped: 0, Total: 21, Duration: ~44 s   - GuyStore.Tests.E2E.dll

Notes