webui: Migrate from @open-wc/testing to Playwright
diff --git a/loop/webui/src/web-components/sketch-container-status.test.ts b/loop/webui/src/web-components/sketch-container-status.test.ts
index 1eae4ee..4a0c397 100644
--- a/loop/webui/src/web-components/sketch-container-status.test.ts
+++ b/loop/webui/src/web-components/sketch-container-status.test.ts
@@ -1,209 +1,161 @@
-import { html, fixture, expect } from "@open-wc/testing";
-import "./sketch-container-status";
-import type { SketchContainerStatus } from "./sketch-container-status";
+import { test, expect } from "@sand4rt/experimental-ct-web";
+import { SketchContainerStatus } from "./sketch-container-status";
 import { State } from "../types";
 
-describe("SketchContainerStatus", () => {
-  // Mock complete state for testing
-  const mockCompleteState: State = {
-    hostname: "test-host",
-    working_dir: "/test/dir",
-    initial_commit: "abcdef1234567890",
-    message_count: 42,
+// Mock complete state for testing
+const mockCompleteState: State = {
+  hostname: "test-host",
+  working_dir: "/test/dir",
+  initial_commit: "abcdef1234567890",
+  message_count: 42,
+  os: "linux",
+  title: "Test Session",
+  total_usage: {
+    input_tokens: 1000,
+    output_tokens: 2000,
+    cache_read_input_tokens: 300,
+    cache_creation_input_tokens: 400,
+    total_cost_usd: 0.25,
+  },
+};
+
+test("render props", async ({ mount }) => {
+  const component = await mount(SketchContainerStatus, {
+    props: {
+      state: mockCompleteState,
+    },
+  });
+  await expect(component.locator("#hostname")).toContainText(
+    mockCompleteState.hostname,
+  );
+  // Check that all expected elements exist
+  await expect(component.locator("#workingDir")).toContainText(
+    mockCompleteState.working_dir,
+  );
+  await expect(component.locator("#initialCommit")).toContainText(
+    mockCompleteState.initial_commit.substring(0, 8),
+  );
+
+  await expect(component.locator("#messageCount")).toContainText(
+    mockCompleteState.message_count + "",
+  );
+  await expect(component.locator("#inputTokens")).toContainText(
+    mockCompleteState.total_usage.input_tokens + "",
+  );
+  await expect(component.locator("#outputTokens")).toContainText(
+    mockCompleteState.total_usage.output_tokens + "",
+  );
+
+  await expect(component.locator("#cacheReadInputTokens")).toContainText(
+    mockCompleteState.total_usage.cache_read_input_tokens + "",
+  );
+  await expect(component.locator("#cacheCreationInputTokens")).toContainText(
+    mockCompleteState.total_usage.cache_creation_input_tokens + "",
+  );
+  await expect(component.locator("#totalCost")).toContainText(
+    "$" + mockCompleteState.total_usage.total_cost_usd.toFixed(2),
+  );
+});
+
+test("renders with undefined state", async ({ mount }) => {
+  const component = await mount(SketchContainerStatus, {});
+
+  // Elements should exist but be empty
+  await expect(component.locator("#hostname")).toContainText("");
+  await expect(component.locator("#workingDir")).toContainText("");
+  await expect(component.locator("#initialCommit")).toContainText("");
+  await expect(component.locator("#messageCount")).toContainText("");
+  await expect(component.locator("#inputTokens")).toContainText("");
+  await expect(component.locator("#outputTokens")).toContainText("");
+  await expect(component.locator("#totalCost")).toContainText("$0.00");
+});
+
+test("renders with partial state data", async ({ mount }) => {
+  const partialState: Partial<State> = {
+    hostname: "partial-host",
+    message_count: 10,
     os: "linux",
-    title: "Test Session",
+    title: "Partial Test",
     total_usage: {
-      input_tokens: 1000,
-      output_tokens: 2000,
-      cache_read_input_tokens: 300,
-      cache_creation_input_tokens: 400,
-      total_cost_usd: 0.25,
+      input_tokens: 500,
     },
   };
 
-  it("renders with complete state data", async () => {
-    const el: SketchContainerStatus = await fixture(html`
-      <sketch-container-status
-        .state=${mockCompleteState}
-      ></sketch-container-status>
-    `);
-
-    // Check that all expected elements exist
-    expect(el.shadowRoot!.querySelector("#hostname")).to.exist;
-    expect(el.shadowRoot!.querySelector("#workingDir")).to.exist;
-    expect(el.shadowRoot!.querySelector("#initialCommit")).to.exist;
-    expect(el.shadowRoot!.querySelector("#messageCount")).to.exist;
-    expect(el.shadowRoot!.querySelector("#inputTokens")).to.exist;
-    expect(el.shadowRoot!.querySelector("#outputTokens")).to.exist;
-    expect(el.shadowRoot!.querySelector("#cacheReadInputTokens")).to.exist;
-    expect(el.shadowRoot!.querySelector("#cacheCreationInputTokens")).to.exist;
-    expect(el.shadowRoot!.querySelector("#totalCost")).to.exist;
-
-    // Verify content of displayed elements
-    expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal(
-      "test-host",
-    );
-    expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
-      "/test/dir",
-    );
-    expect(
-      el.shadowRoot!.querySelector("#initialCommit")!.textContent,
-    ).to.equal("abcdef12"); // Only first 8 chars
-    expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
-      "42",
-    );
-    expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
-      "1000",
-    );
-    expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
-      "2000",
-    );
-    expect(
-      el.shadowRoot!.querySelector("#cacheReadInputTokens")!.textContent,
-    ).to.equal("300");
-    expect(
-      el.shadowRoot!.querySelector("#cacheCreationInputTokens")!.textContent,
-    ).to.equal("400");
-    expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
-      "$0.25",
-    );
+  const component = await mount(SketchContainerStatus, {
+    props: {
+      state: partialState as State,
+    },
   });
 
-  it("renders with undefined state", async () => {
-    const el: SketchContainerStatus = await fixture(html`
-      <sketch-container-status></sketch-container-status>
-    `);
+  // Check that elements with data are properly populated
+  await expect(component.locator("#hostname")).toContainText("partial-host");
+  await expect(component.locator("#messageCount")).toContainText("10");
+  await expect(component.locator("#inputTokens")).toContainText("500");
 
-    // Elements should exist but be empty
-    expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal("");
-    expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
-      "",
-    );
-    expect(
-      el.shadowRoot!.querySelector("#initialCommit")!.textContent,
-    ).to.equal("");
-    expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
-      "",
-    );
-    expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
-      "",
-    );
-    expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
-      "",
-    );
-    expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
-      "$0.00",
-    );
-  });
+  // Check that elements without data are empty
+  await expect(component.locator("#workingDir")).toContainText("");
+  await expect(component.locator("#initialCommit")).toContainText("");
+  await expect(component.locator("#outputTokens")).toContainText("");
+  await expect(component.locator("#totalCost")).toContainText("$0.00");
+});
 
-  it("renders with partial state data", async () => {
-    const partialState: Partial<State> = {
-      hostname: "partial-host",
-      message_count: 10,
-      os: "linux",
-      title: "Partial Test",
+test("handles cost formatting correctly", async ({ mount }) => {
+  // Test with different cost values
+  const testCases = [
+    { cost: 0, expected: "$0.00" },
+    { cost: 0.1, expected: "$0.10" },
+    { cost: 1.234, expected: "$1.23" },
+    { cost: 10.009, expected: "$10.01" },
+  ];
+
+  for (const testCase of testCases) {
+    const stateWithCost = {
+      ...mockCompleteState,
       total_usage: {
-        input_tokens: 500,
+        ...mockCompleteState.total_usage,
+        total_cost_usd: testCase.cost,
       },
     };
 
-    const el: SketchContainerStatus = await fixture(html`
-      <sketch-container-status
-        .state=${partialState as State}
-      ></sketch-container-status>
-    `);
+    const component = await mount(SketchContainerStatus, {
+      props: {
+        state: stateWithCost,
+      },
+    });
+    await expect(component.locator("#totalCost")).toContainText(
+      testCase.expected,
+    );
+    await component.unmount();
+  }
+});
 
-    // Check that elements with data are properly populated
-    expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal(
-      "partial-host",
-    );
-    expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
-      "10",
-    );
-    expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
-      "500",
-    );
+test("truncates commit hash to 8 characters", async ({ mount }) => {
+  const stateWithLongCommit = {
+    ...mockCompleteState,
+    initial_commit: "1234567890abcdef1234567890abcdef12345678",
+  };
 
-    // Check that elements without data are empty
-    expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
-      "",
-    );
-    expect(
-      el.shadowRoot!.querySelector("#initialCommit")!.textContent,
-    ).to.equal("");
-    expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
-      "",
-    );
-    expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
-      "$0.00",
-    );
+  const component = await mount(SketchContainerStatus, {
+    props: {
+      state: stateWithLongCommit,
+    },
   });
 
-  it("handles cost formatting correctly", async () => {
-    // Test with different cost values
-    const testCases = [
-      { cost: 0, expected: "$0.00" },
-      { cost: 0.1, expected: "$0.10" },
-      { cost: 1.234, expected: "$1.23" },
-      { cost: 10.009, expected: "$10.01" },
-    ];
+  await expect(component.locator("#initialCommit")).toContainText("12345678");
+});
 
-    for (const testCase of testCases) {
-      const stateWithCost = {
-        ...mockCompleteState,
-        total_usage: {
-          ...mockCompleteState.total_usage,
-          total_cost_usd: testCase.cost,
-        },
-      };
-
-      const el: SketchContainerStatus = await fixture(html`
-        <sketch-container-status
-          .state=${stateWithCost}
-        ></sketch-container-status>
-      `);
-
-      expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
-        testCase.expected,
-      );
-    }
+test("has correct link elements", async ({ mount }) => {
+  const component = await mount(SketchContainerStatus, {
+    props: {
+      state: mockCompleteState,
+    },
   });
 
-  it("truncates commit hash to 8 characters", async () => {
-    const stateWithLongCommit = {
-      ...mockCompleteState,
-      initial_commit: "1234567890abcdef1234567890abcdef12345678",
-    };
+  // Check for logs link
+  const logsLink = component.locator("a").filter({ hasText: "Logs" });
+  await expect(logsLink).toHaveAttribute("href", "logs");
 
-    const el: SketchContainerStatus = await fixture(html`
-      <sketch-container-status
-        .state=${stateWithLongCommit}
-      ></sketch-container-status>
-    `);
-
-    expect(
-      el.shadowRoot!.querySelector("#initialCommit")!.textContent,
-    ).to.equal("12345678");
-  });
-
-  it("has correct link elements", async () => {
-    const el: SketchContainerStatus = await fixture(html`
-      <sketch-container-status
-        .state=${mockCompleteState}
-      ></sketch-container-status>
-    `);
-
-    const links = Array.from(el.shadowRoot!.querySelectorAll("a"));
-    expect(links.length).to.equal(2);
-
-    // Check for logs link
-    const logsLink = links.find((link) => link.textContent === "Logs");
-    expect(logsLink).to.exist;
-    expect(logsLink!.getAttribute("href")).to.equal("logs");
-
-    // Check for download link
-    const downloadLink = links.find((link) => link.textContent === "Download");
-    expect(downloadLink).to.exist;
-    expect(downloadLink!.getAttribute("href")).to.equal("download");
-  });
+  // Check for download link
+  const downloadLink = component.locator("a").filter({ hasText: "Download" });
+  await expect(downloadLink).toHaveAttribute("href", "download");
 });