using Microsoft.Playwright; using Tests.E2e.Infra; using Tests.E2e.PageObjectModels; using Xunit.Abstractions; namespace Tests.E2e; /// /// Coverage for /yaml/import and the /yaml config view. /// Issue #308: connections in an imported document were parsed but silently /// dropped — they never showed in the preview and never reached the config. /// The preview assertions here are the regression guard; the /yaml /// round-trip proves the connection actually persisted. /// public class YamlImportTests( PlaywrightFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, output) { private readonly PlaywrightFixture _fixture = fixture; private readonly ITestOutputHelper _output = output; private static string TwoSwitchesWithConnection(string switchA, string switchB) => $""" version: 3 resources: - kind: Switch name: {switchA} ports: - type: rj45 speed: 1 count: 4 - kind: Switch name: {switchB} ports: - type: rj45 speed: 1 count: 4 connections: - a: resource: {switchA} portGroup: 0 portIndex: 0 b: resource: {switchB} portGroup: 0 portIndex: 0 """; // ============================================================= // Connections reach the preview (issue #308 regression) // ============================================================= [Fact] public async Task Import_Preview_Lists_Connections_From_The_Document() { (IBrowserContext context, IPage page) = await CreatePageAsync(); var switchA = $"e2e-ia-{Guid.NewGuid():N}"[..14]; var switchB = $"e2e-ib-{Guid.NewGuid():N}"[..14]; try { var import = new YamlImportPom(page); await import.GotoAsync(_fixture.BaseUrl); await import.PasteAsync(TwoSwitchesWithConnection(switchA, switchB)); await import.AssertNoErrorAsync(); // Before the #308 fix this section never rendered: the connection // was dropped between parsing and the diff. await import.AssertConnectionsSummaryVisibleAsync(); await Assertions.Expect(import.ConnectionsAdded).ToHaveCountAsync(1); await import.AssertConnectionAddedContainsAsync(switchA); await import.AssertConnectionAddedContainsAsync(switchB); } catch (Exception) { await DumpAsync(page); throw; } finally { await context.CloseAsync(); } } // ============================================================= // Applying the import persists the connection // ============================================================= [Fact] public async Task Applying_An_Import_Persists_Resources_And_Their_Connection() { (IBrowserContext context, IPage page) = await CreatePageAsync(); var switchA = $"e2e-pa-{Guid.NewGuid():N}"[..14]; var switchB = $"e2e-pb-{Guid.NewGuid():N}"[..14]; try { var import = new YamlImportPom(page); await import.GotoAsync(_fixture.BaseUrl); await import.PasteAsync(TwoSwitchesWithConnection(switchA, switchB)); await import.AssertNoErrorAsync(); await import.ApplyAsync(); // The saved config is the source of truth — read it back rather // than trusting the preview we just asserted on. await page.GotoAsync($"{_fixture.BaseUrl}/yaml"); ILocator yaml = page.GetByTestId("yaml-file-content"); await Assertions.Expect(yaml).ToBeVisibleAsync(); await Assertions.Expect(yaml).ToContainTextAsync(switchA); await Assertions.Expect(yaml).ToContainTextAsync(switchB); await Assertions.Expect(yaml).ToContainTextAsync("connections:"); } catch (Exception) { await DumpAsync(page); throw; } finally { await context.CloseAsync(); } } // ============================================================= // Malformed input is reported, not swallowed // ============================================================= [Fact] public async Task Import_Surfaces_An_Error_For_Malformed_Yaml() { (IBrowserContext context, IPage page) = await CreatePageAsync(); try { var import = new YamlImportPom(page); await import.GotoAsync(_fixture.BaseUrl); // Unclosed bracket — a parser error rather than a schema violation. await import.PasteAsync(""" version: 3 resources: [ - kind: Switch """); await import.AssertErrorShownAsync(); // A broken document must not be applyable. await Assertions.Expect(import.ApplyButton).ToBeDisabledAsync(); } catch (Exception) { await DumpAsync(page); throw; } finally { await context.CloseAsync(); } } // ============================================================= // The prerender race (regression guard for the CI flake on #339) // ============================================================= /// /// The import page has the same prerender race the add form had (b8f6d23): /// text filled before the circuit attaches never fires the oninput diff, so the /// Apply button stays disabled forever. CI lost this race intermittently; the /// injected latency loses it every time, proving GotoAsync's circuit wait fixes it. /// [Fact] public async Task Importing_Works_Before_The_Circuit_Has_Warmed_Up() { (IBrowserContext context, IPage page) = await CreatePageAsync(); await BlazorLatency.AddAsync(page, TimeSpan.FromMilliseconds(300)); var switchA = $"e2e-ra-{Guid.NewGuid():N}"[..14]; var switchB = $"e2e-rb-{Guid.NewGuid():N}"[..14]; try { var import = new YamlImportPom(page); await import.GotoAsync(_fixture.BaseUrl); await import.PasteAsync(TwoSwitchesWithConnection(switchA, switchB)); // The regression: without GotoAsync waiting for the circuit, the fill above // is lost to the prerendered page, the diff never runs, and Apply stays // disabled. Asserting it becomes enabled is the whole point — persistence is // already covered by the non-latency test, and a post-apply navigation would // only reintroduce flakiness under the injected latency. await Assertions.Expect(import.ApplyButton).ToBeEnabledAsync(); } catch (Exception) { await DumpAsync(page); throw; } finally { await context.CloseAsync(); } } private async Task DumpAsync(IPage page) { _output.WriteLine("TEST FAILED — Capturing diagnostics"); _output.WriteLine($"Current URL: {page.Url}"); var html = await page.ContentAsync(); _output.WriteLine("==== DOM SNAPSHOT START ===="); _output.WriteLine(html); _output.WriteLine("==== DOM SNAPSHOT END ===="); } }