Repository navigation
feat(ui): a Baselines page and a device's baseline #1245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
+738
−113
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
67626e4
test: a Baselines page lists each baseline the worker holds and a sav…
V3RON 226e198
feat(ui): a Baselines page lists the worker's baselines and a save sh…
V3RON 2919cc4
test: each of baseline.saved, baseline.removed and baseline.stale mar…
V3RON 6d957d2
test: baseline rows are ordered by name then worker and mark a stale …
V3RON 791b3cf
feat(ui): baseline rows are ordered by name then worker and carry the…
V3RON c389e9d
test: the Baselines page shows versions, the Worker column, stale rea…
V3RON 6d4c288
feat(ui): the Baselines page shows the Worker and Stale columns and a…
V3RON 2cbc745
docs: the Baselines page in the console manual, the design doc, the w…
V3RON d5795bd
docs: the console's Baselines section links the website's Baselines p…
V3RON 045d17a
docs: the drawer lists state before the baseline; test: the stale squ…
V3RON 1ae47ae
Merge remote-tracking branch 'origin/task/1208' into HEAD
V3RON 519e2c0
docs: ADR 0061 is Accepted (#1209)
V3RON 72dbb66
fix(android): the daemon leaves its adb server running while an emula…
V3RON d2ff948
test(e2e): the tunnel's leftover read waits until adb drops the delet…
V3RON 631e059
test(e2e): the Android smoke test aims a worker's simlock adb at the …
V3RON d4b3f1a
fix(ports): a file that vanishes while a directory is sized counts as…
V3RON 0926751
test(e2e): the iOS baseline test finds its clone among the seeds the …
V3RON d79c51f
test(e2e): the iOS baseline marker lives in data/Documents, since a s…
V3RON c517303
test(e2e): teardown waits up to 60 s for the daemon's socket, as a st…
V3RON 741cdd3
test(e2e): the Android baseline test names each lease's serial, since…
V3RON 0d38b33
fix(android): a device provisioned from a baseline source is a copy o…
V3RON b4bcd22
fix(e2e): a second teardown of a home never replaces the daemon log t…
V3RON 4301591
test(e2e): the agent-device suite's cleanup hook outlives waitForFree…
V3RON File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...-every-virtual-device-is-a-copy-of-a-source-and-a-baseline-is-a-named-source.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import type { Locator, Page } from "@playwright/test"; | ||
|
|
||
| import { answer } from "./stub-answers.js"; | ||
| import { | ||
| disposeFleet, | ||
| expect, | ||
| type RunningDaemon, | ||
| signIn, | ||
| startDaemon, | ||
| startFleet, | ||
| test, | ||
| } from "./fixtures.js"; | ||
|
|
||
| const IOS = { availableOsVersions: ["18.4"], knownModels: ["iPhone 16"] }; | ||
| const LEASE = ["lease", "--platform", "ios", "--device", "iPhone 16", "--os", "18.4"] as const; | ||
|
|
||
| async function leaseId(daemon: RunningDaemon, agent: string, extra: readonly string[] = []) { | ||
| const granted = await daemon.cli([...LEASE, "--agent-id", agent, "--detach", ...extra]); | ||
| expect(granted.code, granted.stderr).toBe(0); | ||
| return (granted.json as { lease: { id: string } }).lease.id; | ||
| } | ||
|
|
||
| async function save(daemon: RunningDaemon, name: string, lease: string, note?: string) { | ||
| const args = ["baseline", "save", name, "--lease", lease]; | ||
| const saved = await daemon.cli(note === undefined ? args : [...args, "--note", note]); | ||
| expect(saved.code, saved.stderr).toBe(0); | ||
| } | ||
|
|
||
| async function openBaselines(page: Page, daemon: RunningDaemon): Promise<void> { | ||
| await page.goto(new URL("/baselines", daemon.url).toString()); | ||
| await signIn(page, daemon.tokens.operator); | ||
| await expect(page.getByRole("heading", { level: 1, name: "Baselines" })).toBeVisible(); | ||
| } | ||
|
|
||
| function row(page: Page, name: string): Locator { | ||
| return page.locator("main tbody tr").filter({ hasText: name }); | ||
| } | ||
|
|
||
| test.describe("the Baselines page", () => { | ||
| test("lists each baseline the worker holds, a save shows its row with no reload", async ({ | ||
| page, | ||
| }) => { | ||
| const host = await startDaemon("worker", { driverScript: { ios: IOS } }); | ||
| try { | ||
| const lease = await leaseId(host, "saver"); | ||
| await openBaselines(page, host); | ||
| await expect(page.getByText("No baselines.")).toBeVisible(); | ||
|
|
||
| await save(host, "checkout", lease, "app 4.2"); | ||
|
|
||
| const checkout = row(page, "checkout"); | ||
| await expect(checkout).toBeVisible(); | ||
| await expect(checkout).toContainText("iOS"); | ||
| await expect(checkout).toContainText("iPhone 16"); | ||
| await expect(checkout).toContainText("18.4"); | ||
| await expect(checkout).toContainText("app 4.2"); | ||
| await expect(checkout).toContainText("1 MiB"); | ||
| await expect(checkout).toContainText("ago"); | ||
| } finally { | ||
| await host.dispose(); | ||
| } | ||
| }); | ||
|
|
||
| test("a second save shows version 2 in place, a lease naming the baseline shows it in the device's drawer, and a removal takes the row away with no reload", async ({ | ||
| page, | ||
| }) => { | ||
| const host = await startDaemon("worker", { driverScript: { ios: IOS } }); | ||
| try { | ||
| await save(host, "checkout", await leaseId(host, "first"), "app 4.2"); | ||
| await openBaselines(page, host); | ||
| await expect(row(page, "checkout").getByRole("cell").nth(1)).toHaveText("1"); | ||
|
|
||
| await save(host, "checkout", await leaseId(host, "second")); | ||
| await expect(row(page, "checkout").getByRole("cell").nth(1)).toHaveText("2"); | ||
| await expect(page.locator("main tbody tr")).toHaveCount(1); | ||
|
|
||
| const copy = await leaseId(host, "copier", ["--baseline", "checkout"]); | ||
| const plain = await leaseId(host, "plain"); | ||
| const view = await workers(host); | ||
| const onCopy = view.leases.find((held) => held.id === copy); | ||
| const onPlain = view.leases.find((held) => held.id === plain); | ||
| await openDrawer(page, host, view.id, onCopy?.deviceId ?? ""); | ||
| await expect(fact(page, "Baseline")).toHaveText("checkout v2"); | ||
| await openDrawer(page, host, view.id, onPlain?.deviceId ?? ""); | ||
| await expect(page.getByRole("dialog").locator("dt", { hasText: "Runtime" })).toBeVisible(); | ||
| await expect(page.getByRole("dialog").locator("dt", { hasText: "Baseline" })).toHaveCount(0); | ||
|
|
||
| await page.goto(new URL("/baselines", host.url).toString()); | ||
| for (const id of [copy, plain]) { | ||
| const released = await host.cli(["release", id]); | ||
| expect(released.code, released.stderr).toBe(0); | ||
| } | ||
| const removed = await host.cli(["baseline", "remove", "checkout", "--yes"]); | ||
| expect(removed.code, removed.stderr).toBe(0); | ||
| await expect(page.getByText("No baselines.")).toBeVisible(); | ||
| } finally { | ||
| await host.dispose(); | ||
| } | ||
| }); | ||
|
|
||
| test("on a gateway, the Worker column names each worker's baseline and a relayed save shows its row", async ({ | ||
| page, | ||
| }) => { | ||
| const fleet = await startFleet([ | ||
| { driverScript: { ios: IOS }, label: "worker-a" }, | ||
| { driverScript: { ios: IOS }, label: "worker-b" }, | ||
| ]); | ||
| try { | ||
| const [first, second] = fleet.workers as [RunningDaemon, RunningDaemon]; | ||
| await openBaselines(page, fleet.gateway); | ||
|
|
||
| await save(first, "alpha", await leaseId(first, "a")); | ||
| await expect(page.getByRole("columnheader", { name: "Worker" })).toBeVisible(); | ||
| await expect(row(page, "alpha")).toContainText("worker-a"); | ||
| await save(second, "beta", await leaseId(second, "b")); | ||
| await expect(row(page, "beta")).toContainText("worker-b"); | ||
| await expect(page.locator("main tbody tr")).toHaveCount(2); | ||
| } finally { | ||
| await disposeFleet(fleet); | ||
| } | ||
| }); | ||
|
|
||
| test("a stale baseline shows the square, stale and its reason word for word, an entry with no size shows a dash, and 26 entries page at 25", async ({ | ||
| page, | ||
| }) => { | ||
| const host = await startDaemon("worker", { driverScript: { ios: IOS } }); | ||
| try { | ||
| const reason = "runtime com.apple.CoreSimulator.SimRuntime.iOS-27-0 is not installed"; | ||
| const entries = Array.from({ length: 26 }, (_, index) => ({ | ||
| mode: "full", | ||
| model: "iPhone 16", | ||
| name: `base-${String(index).padStart(2, "0")}`, | ||
| osVersion: "18.4", | ||
| platform: "ios", | ||
| savedAt: new Date(Date.now() - 3_600_000).toISOString(), | ||
| version: 1, | ||
| ...(index === 0 ? { staleReason: reason } : { sizeBytes: 2 ** 20 }), | ||
| })); | ||
| await answer(page, "/v1/baselines", () => ({ baselines: entries })); | ||
| await openBaselines(page, host); | ||
|
|
||
| await expect(page.locator("main tbody tr")).toHaveCount(25); | ||
| await expect(row(page, "base-25")).toHaveCount(0); | ||
| const stale = row(page, "base-00"); | ||
| await expect(stale).toContainText(`stale · ${reason}`); | ||
| await expect(stale.locator(".status.status-warn")).toHaveText("stale"); | ||
| await expect(stale).toContainText("—"); | ||
| await page.getByRole("button", { name: "Next" }).click(); | ||
| await expect(row(page, "base-25")).toBeVisible(); | ||
| } finally { | ||
| await host.dispose(); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| interface WorkerFacts { | ||
| readonly id: string; | ||
| readonly leases: readonly { readonly id: string; readonly deviceId: string }[]; | ||
| } | ||
|
|
||
| async function workers(daemon: RunningDaemon): Promise<WorkerFacts> { | ||
| const listed = await daemon.cli(["worker", "list", "--json"]); | ||
| expect(listed.code, listed.stderr).toBe(0); | ||
| const [view] = (listed.json as { workers: WorkerFacts[] }).workers; | ||
| if (view === undefined) throw new Error("no worker"); | ||
| return view; | ||
| } | ||
|
|
||
| async function openDrawer( | ||
| page: Page, | ||
| daemon: RunningDaemon, | ||
| workerId: string, | ||
| deviceId: string, | ||
| ): Promise<void> { | ||
| const path = `/fleet/${encodeURIComponent(workerId)}/${encodeURIComponent(deviceId)}?tab=devices`; | ||
| await page.goto(new URL(path, daemon.url).toString()); | ||
| await expect(page.getByRole("dialog")).toBeVisible(); | ||
| } | ||
|
|
||
| function fact(scope: Page, label: string): Locator { | ||
| return scope | ||
| .getByRole("dialog") | ||
| .locator("dt", { hasText: label }) | ||
| .locator("xpath=following-sibling::dd[1]"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Require a successful read from the third device.
If
adbOnfails because the third serial is invalid or offline, the existing nonzero-exit assertion passes without checking the file. List the directory successfully and assert thatt2is absent. Also reject a missing address instead of passing"". (raw.githubusercontent.com)