Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
b8c9bc5
Add native SDK planner data adapter foundation
callumalpass Oct 10, 2026
9ccf195
planner: integrate qualified native view-source writes
callumalpass Oct 10, 2026
b240e04
fix: preserve planner recovery witnesses and source write safety
callumalpass Oct 10, 2026
94fc227
chore: pin qualified corrected native SDK archive
callumalpass Oct 10, 2026
40ffb3b
chore: pin qualified shared read-helper SDK successor
callumalpass Oct 10, 2026
daca1d2
chore: pin qualified explicit-environment SDK successor
callumalpass Oct 10, 2026
44adbe2
add public SDK sign-in UI with explicit collection boundary (#8)
callumalpass Oct 10, 2026
bc06a3e
Keep and close the owned Connect popup after approval (#9)
callumalpass Oct 10, 2026
c2f93e1
Keep leaving sample tasks consistent across reloads (#10)
callumalpass Oct 10, 2026
5f63bfd
Pin release-qualified SDK819 archive (#11)
callumalpass Oct 10, 2026
cf7fb39
Pin release-qualified SDK837 archive (#12)
callumalpass Oct 11, 2026
4ae4834
Clarify shared SDK entry ownership and source-only UI boundary (#13)
callumalpass Oct 11, 2026
09d51b2
test: exercise Planner with the native SDK client
callumalpass Oct 11, 2026
abd2401
feat: port Planner UI to the public SDK entry surface
callumalpass Oct 11, 2026
d6fae6f
test: cover Planner SDK root readiness and scoped lifetime
callumalpass Oct 11, 2026
3ef6578
Pin Planner to original 563dc868 SDK dev archive
callumalpass Oct 11, 2026
20e68b9
Fix Planner drift against the SDK563 public application entry
callumalpass Oct 11, 2026
263efe9
Repin Planner to the ab00e5eb SDK dev archive
callumalpass Oct 11, 2026
85c5f1c
Repin Planner to the 7193fd0d SDK dev archive
callumalpass Oct 11, 2026
5512e6f
Wire Planner's LAB builds to the live SDK entry
callumalpass Oct 11, 2026
aa6eee2
Repin Planner to the d53d7d76 SDK dev archive
callumalpass Oct 11, 2026
580d93b
Repin Planner to the df091839 SDK dev archive
callumalpass Oct 11, 2026
298a96b
WIP: write Planner task changes through the tasknotes.task contract
callumalpass Oct 11, 2026
95ad50c
Repin Planner to the 38001079 SDK dev archive
callumalpass Oct 11, 2026
29ed419
Merge claude/planner-contract-writes: write task edits through the ta…
callumalpass Oct 11, 2026
bf3d5eb
planner: pin original native n1 SDK for LAB preparation
callumalpass Oct 11, 2026
4f6a4f4
planner: replace settled readiness spinners with recovery actions
callumalpass Oct 11, 2026
5ac3b41
planner: pin original DEV LAB SDK 4b955381
callumalpass Oct 11, 2026
af9e0b5
planner: pin original DEV LAB SDK 6a576a5f
callumalpass Oct 11, 2026
50c1054
Planner: SDK describeError screens for blocked, readiness and incompa…
callumalpass Oct 11, 2026
8c09083
planner: review setup changes before applying them
callumalpass Oct 11, 2026
a2bf46e
planner: plan setup once under StrictMode replays
callumalpass Oct 11, 2026
4159133
planner: setup review reports unconfirmed outcomes, skips empty plans…
callumalpass Oct 11, 2026
09cafe8
planner: consolidate public SDK launch entry and exact 41c intake
callumalpass Oct 11, 2026
1e2c3f3
Pin original SDK 4c and keep sign-in recovery actionable
callumalpass Oct 11, 2026
63cccdf
Pin SDK 48b9f094
callumalpass Oct 11, 2026
6937036
Pin SDK 20c347ad
callumalpass Oct 12, 2026
53f2411
Show mdbase problem copy instead of raw runtime text
callumalpass Oct 12, 2026
ae660cc
Pin SDK ac3bff98
callumalpass Oct 12, 2026
d718532
Require and provision the obsidian.base contract for Bases views
callumalpass Oct 12, 2026
a2f5775
Keep the setup review open while setup applies
callumalpass Oct 12, 2026
8a7fca4
Pin SDK 1f7b4f7a
callumalpass Oct 12, 2026
4023a9f
Pin SDK 8bc12525
callumalpass Oct 12, 2026
7ba090e
Pin SDK cf294bdb
callumalpass Oct 12, 2026
f600eb3
Pin SDK a965bd26
callumalpass Oct 12, 2026
81d8f35
provision tasknotes.task 0.3.0-rc.18, the pack TaskNotes ships
callumalpass Oct 12, 2026
fe44d2b
Hide the Sign in action while a signed-in collection needs setup or a…
callumalpass Oct 12, 2026
02aa8a8
Pin SDK 5f2c3d5d
callumalpass Oct 12, 2026
c8df792
Pin SDK 8b18a6c7
callumalpass Oct 12, 2026
7c7ae23
Pin SDK 0f9a3614
callumalpass Oct 12, 2026
d42a0ba
Pin SDK 1dddb04c
callumalpass Oct 12, 2026
7d9d7ec
Pin SDK 5710ab13
callumalpass Oct 12, 2026
498c976
Trap focus and support Escape in the save view dialog
callumalpass Oct 12, 2026
023fbaf
Pin SDK 39ba2b8a
callumalpass Oct 12, 2026
663f774
Pin SDK 53643b29
callumalpass Oct 12, 2026
6d458c7
Pin SDK b1c47a32
callumalpass Oct 12, 2026
c5d8b68
Pin SDK fb3308a8
callumalpass Oct 12, 2026
d614beb
Sign out through the SDK so a reload stays signed out
callumalpass Oct 12, 2026
4c93122
Serve deep routes on Cloudflare Pages
callumalpass Oct 12, 2026
658a72a
Use the shared mdbase collection UI
callumalpass Oct 12, 2026
7f7ee72
Don't follow page visibility through this SDK cut
callumalpass Oct 12, 2026
3620e3e
Pin SDK 4728a54c
callumalpass Oct 12, 2026
03478ac
Follow page visibility again
callumalpass Oct 12, 2026
25277bd
Pin shared UI 5df37dc9
callumalpass Oct 12, 2026
04816f6
Let the shared gate start the entry; show sample tasks on sign-in only
callumalpass Oct 12, 2026
2d7b15c
Leave unconfirmed writes to the SDK's write ledger
callumalpass Oct 12, 2026
da6ca19
Pin SDK 0aeaada6
callumalpass Oct 12, 2026
a50587f
Pin SDK 47225957
callumalpass Oct 12, 2026
ba834de
Pin SDK fddaa4c1
callumalpass Oct 12, 2026
8c1a8be
Pin SDK fd0c4a67
callumalpass Oct 12, 2026
975c999
Show when changes are saved on this device and waiting for sync
callumalpass Oct 12, 2026
3ee51f8
Pin @mdbase-dev/ui 975fc4a0
callumalpass Oct 12, 2026
b2230d5
Pin SDK 013c7e56
callumalpass Oct 13, 2026
3ca99a0
Pin @mdbase-dev/ui 7ff0ce02
callumalpass Oct 13, 2026
27a66d8
Save a new view once when its save is retried
callumalpass Oct 13, 2026
c062455
Pin SDK 608bb0d5
callumalpass Oct 13, 2026
4b7bab5
Pin SDK a8fdbdce
callumalpass Oct 13, 2026
940924c
Pin @mdbase-dev/ui 27fc243f
callumalpass Oct 13, 2026
0441fe1
Deliver the Private collection consent popup's return in LAB builds
callumalpass Oct 13, 2026
7a85967
Pin SDK fb9691b0
callumalpass Oct 13, 2026
6b00342
Show honest offline state and serve a 404 for unknown paths
callumalpass Oct 13, 2026
e254798
Pin SDK d230034f
callumalpass Oct 13, 2026
6707977
Keep a new view's save identity when it is renamed and retried
callumalpass Oct 13, 2026
a19f38e
Declare collection file actions for record access
callumalpass Oct 13, 2026
38502a0
Pin SDK 1629d11e
callumalpass Oct 13, 2026
7d1c133
Render SDK storage warnings and shared Private collection entry
callumalpass Oct 13, 2026
9b4a5c0
Pin SDK e00c1ff8
callumalpass Oct 13, 2026
4c8cf15
Pin SDK af29edc9
callumalpass Oct 13, 2026
6d661be
Pin SDK 12c28ef1
callumalpass Oct 13, 2026
db9dad0
Pin SDK 23a10ee4
callumalpass Oct 13, 2026
ff07cf4
Pin SDK ca8f0682
callumalpass Oct 13, 2026
d8b9e27
fix: cache offline app shell
callumalpass Oct 13, 2026
69397f6
fix: support title scheduling and phone view saving
callumalpass Oct 13, 2026
52bacbd
Pin SDK abc08f3e
callumalpass Oct 13, 2026
32e8269
Pin SDK e4450c26
callumalpass Oct 13, 2026
96fdaf0
fix: synchronize task inspector state and wrap phone ledger titles
callumalpass Oct 13, 2026
bfe7b57
Pin SDK 3491a619
callumalpass Oct 13, 2026
cdd5d4d
Pin SDK 6f2b1e51
callumalpass Oct 13, 2026
b0bc199
fix: flag reversed task schedules and reject adjacent inversions
callumalpass Oct 13, 2026
78d949d
Revalidate unchanged task bindings before follow-up edits
callumalpass Oct 13, 2026
cfebc7b
Keep full schedule dates readable on phones
callumalpass Oct 13, 2026
779dee3
Pin SDK 4c10b9a5
callumalpass Oct 13, 2026
7c72b24
Pin SDK 5c7b9592
callumalpass Oct 13, 2026
4b239c4
Pin SDK 6f14ebe1
callumalpass Oct 13, 2026
e8aa462
Pin SDK 0f5724ef
callumalpass Oct 13, 2026
1808f54
Pin SDK 106d36ff
callumalpass Oct 14, 2026
2231345
Pin SDK f1aa67e3
callumalpass Oct 14, 2026
2a663c4
Pin SDK bef3fe45
callumalpass Oct 14, 2026
35c9c47
Pin SDK 3f417b6d
callumalpass Oct 14, 2026
6b5d983
Pin unified SDK 72f8ea6 and defer Native popup close to its original …
callumalpass Oct 14, 2026
b1ab534
Add explicit guarded LAB target to development deployment
callumalpass Oct 14, 2026
eae5780
fix(planner): declare Bases record extensions in normal setup
callumalpass Oct 14, 2026
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
37 changes: 37 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Product

## Register

product

## Users and purpose

TaskNotes users planning task dates and dependencies on a timeline. The task
ledger and timeline represent the same records, not a separate planning database.

## Brand

Use the existing TaskNotes family language: quiet paper, legible type, precise
rules, restrained blue. Preserve the visual system in [DESIGN.md](DESIGN.md).

## Anti-references

Do not introduce a dashboard sidebar or decorative status cards. Do not expose
internal identifiers or protocol terminology in ordinary sign-in messages.

## Principles

- Preserve one authoritative ledger/timeline scroll and task-row geometry.
- Keep task edits and collection readiness honest; transport is not saved proof.
- Sign-in follows the shared SDK lifecycle, with clear plain-language status.
- Preserve the working runtime until native composition is qualified.

## Accessibility

Keep complete control names, keyboard operation, visible focus, light/dark
appearance and reduced motion. Status must be written, not colour-only.

## Sources

Existing README.md and DESIGN.md; the coordinator's decided sign-in copy.
This records existing product context, not a new visual direction.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ Planner discovers Obsidian Bases views with `type: tasknotesPlanner`. Selecting
one executes its standard Base filters and ordering through mdbase. Saving a
view creates or updates a source under `TaskNotes/Views/`, with the current
project, status, priority, completed-task preference, and zoom stored in the
view. TaskNotes can show the same filtered result as a compact list and hand it
view. The normal collection setup declares `md` and `base` in
`settings.record_extensions`, preserving Markdown and existing extensions: the
canonical `obsidian.base` type requires YAML document records, not opaque files.
TaskNotes can show the same filtered result as a compact list and hand it
off to Planner for timeline work.

## Timeline interactions
Expand Down Expand Up @@ -69,7 +72,10 @@ pnpm dev
```

Open [http://127.0.0.1:4174/?demo=1](http://127.0.0.1:4174/?demo=1) for the
sample plan, or remove `?demo=1` to connect an mdbase collection. Use
sample plan, or remove `?demo=1` to connect an mdbase collection. The sample
collection is labelled **Sample tasks**; its changes reset when you leave or
reload. Choose its collection name to return to the connection screen. Leaving
the sample also removes its URL flag, so reload does not reopen it. Use
`127.0.0.1`, not the `localhost` alias: the manifest and callback share this
single development origin.

Expand Down Expand Up @@ -134,6 +140,12 @@ signed release. This v2 draft must not deploy before the qualified v2 writer.
The production declaration assumes `https://planner.tasknotes.dev/`. Update the
homepage, icon, and redirect URI together if the deployment origin changes.

## New SDK migration

The existing Connect session remains the default. A native SDK data adapter is
being prepared separately; it is not yet a browser sign-in mode. See
[its behavior, pinned artifact, and remaining dependencies](docs/new-sdk.md).

## Repository structure

```text
Expand Down
6 changes: 4 additions & 2 deletions docs/capability-v2-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ deletion. These are atomic group meanings, not independently requested operation
permissions. Setup assessment/application is handled by Connect's separate
setup flow, with the existing provisions unchanged.

No `records.create`, `records.delete`, `offline.replica`, `files.*`, or
`background.schedule` is requested. Full-collection authorization remains
No `records.create`, `records.delete`, `offline.replica`, or
`background.schedule` is requested. Collection file actions `list`, `read`,
`replace` and `move` are declared because next accounts approve
`collection.read` and `records.edit` together with their matching file actions. Full-collection authorization remains
necessary for Bases, while the application continues to select and mutate task
records through `tasknotes.task` only. No app capability aliases or fabricated
SDK groups were introduced; session state handling remains SDK-owned.
Expand Down
281 changes: 281 additions & 0 deletions docs/new-sdk.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/offline-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Offline app shell

The production build uses `@mdbase-dev/sdk/offline-shell/build` to precache the
public app shell, scripts, lazy chunks, fonts and packaged WASM as one release.
A previously opened app can reload its shell without connectivity. This does
not grant collection access or make uncached files and remote services available
offline; collection recovery and sync remain the SDK's responsibility.

Updates do not reload open tabs. An incomplete deployment does not replace the
last successfully cached release. Shell caching never stores authentication or
collection API responses.
15 changes: 15 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Unreleased

- Stack schedule date and time controls on phone screens so the full date, including its year, remains readable.

- Flag tasks scheduled after their due date in the ledger instead of displaying a reversed range. Their original dates remain available in the inspector, which also rejects adjacent reversed all-day dates.

- Save schedule edits immediately after changing a task's priority or status, without requiring a reload when the collection's task bindings have not changed.

- Keep the inspector's status, priority and projects in sync when a task changes, including completion from its ledger row, without discarding unsaved date edits.

- Wrap phone-sized ledger titles onto two lines so similar task names can be distinguished. Ledger rows, task bars and dependency lines remain aligned.

- Drag an unscheduled task by its title onto a date in the timeline or its header. Drops outside the timeline explain why the schedule stayed unchanged; clicking a title still opens the task editor.

- Keep Save view available at phone widths as a labelled icon button, instead of hiding it.
48 changes: 48 additions & 0 deletions e2e/fixtures/phone-ledger.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Planner phone fixture</title>
</head>
<body>
<div id="root"></div>
<script type="module">
// Development-only HTML entry. No account, SDK application or transport.
import React from "react";
import { createRoot } from "react-dom/client";
import "@fontsource/atkinson-hyperlegible/400.css";
import "@fontsource/atkinson-hyperlegible/700.css";
import "@fontsource/azeret-mono/400.css";
import "/src/styles.css";
import { Planner } from "/src/components/planner.tsx";
import { DemoPlannerRepository } from "/src/data/demo-repository.ts";

const repository = new DemoPlannerRepository();
const sample = await repository.load();
const open = sample.statuses.find((status) => status.value === "open");
if (!open) throw new Error("The fixture requires an open status");
for (const [index, task] of sample.tasks.slice(0, 2).entries()) {
await repository.updateSchedule(
{
...task,
title: `[test] bugfix3 same-prefix phone task ${index ? "beta" : "alpha"}`,
status: open.value,
statusLabel: open.label,
statusColor: open.color,
completed: false,
projects: [],
blockedBy: [],
},
{ scheduled: undefined, due: undefined },
);
}
createRoot(document.getElementById("root")).render(
React.createElement(Planner, {
repository,
onChangeCollection: async () => undefined,
}),
);
</script>
</body>
</html>
210 changes: 210 additions & 0 deletions e2e/phone-planner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { expect, test } from "@playwright/test";

// These checks use only the in-memory sample collection, never a real account.
test.beforeEach(async ({ context, baseURL }) => {
if (!baseURL) throw new Error("A loopback application URL is required");
const app = new URL(baseURL);
if (!["127.0.0.1", "localhost", "[::1]"].includes(app.hostname)) {
throw new Error("Sample checks must run against a loopback application");
}
await context.route("**/*", (route) =>
new URL(route.request().url()).origin === app.origin
? route.continue()
: route.abort("blockedbyclient"),
);
await context.routeWebSocket("**/*", (socket) => {
if (new URL(socket.url()).host === app.host) socket.connectToServer();
else socket.close();
});
});

for (const width of [320, 390]) {
test(`Save view stays available at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 });
await page.goto("/?demo=1");
await expect(page.locator(".gantt-scroll")).toBeVisible();
const save = page.getByRole("button", { name: "Save view", exact: true });
await expect(save).toBeVisible();
const bounds = await save.boundingBox();
expect(bounds).not.toBeNull();
expect(bounds!.width).toBeGreaterThanOrEqual(44);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width);
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBeLessThanOrEqual(width);
await save.click();
const dialog = page.getByRole("dialog", { name: "Create Planner view" });
await expect(dialog).toBeVisible();
await dialog.getByLabel("View name").fill("[test] Phone plan");
await dialog
.getByRole("button", { name: "Save view", exact: true })
.click();
await expect(dialog).toBeHidden();
await expect(
page
.getByRole("combobox", { name: "Planner view" })
.locator("option:checked"),
).toHaveText("[test] Phone plan");
await expect(
page.getByRole("button", { name: "Save changes", exact: true }),
).toBeVisible();
});
}

test("schedule dates and times have full-width rows on phones", async ({
page,
}) => {
for (const width of [320, 390]) {
await page.setViewportSize({ width, height: 844 });
await page.goto("/?demo=1");
await page
.getByRole("button", { name: /Select to edit or drag/ })
.first()
.click();
const inspector = page.locator(".task-inspector");
for (const field of ["Scheduled", "Due"]) {
const date = inspector.getByLabel(field, { exact: true });
const time = inspector.getByLabel(`${field} time`, { exact: true });
await date.fill("2026-11-07");
const dateBounds = await date.boundingBox();
const timeBounds = await time.boundingBox();
expect(dateBounds).not.toBeNull();
expect(timeBounds).not.toBeNull();
expect(dateBounds!.width).toBeGreaterThanOrEqual(240);
expect(timeBounds!.width).toBeGreaterThanOrEqual(240);
expect(timeBounds!.y).toBeGreaterThanOrEqual(
dateBounds!.y + dateBounds!.height,
);
expect(dateBounds!.x + dateBounds!.width).toBeLessThanOrEqual(width);
}
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBeLessThanOrEqual(width);
}
});

test("an unscheduled title can drop onto a date header and still opens the editor on click", async ({
page,
}) => {
await page.goto("/?demo=1");
const title = page
.getByRole("button", { name: /not scheduled. Select to edit or drag/ })
.first();
await expect(title).toBeVisible();
const name = await title.locator("strong").innerText();
const ledger = page.locator(".task-ledger-main").filter({ hasText: name });
const source = await title.boundingBox();
const header = await page.locator(".timeline-header").boundingBox();
const corner = await page.locator(".gantt-corner").boundingBox();
if (!source || !header || !corner)
throw new Error("Drag geometry unavailable");
await page.mouse.move(
source.x + source.width / 2,
source.y + source.height / 2,
);
await page.mouse.down();
await page.mouse.move(
corner.x + corner.width + 200,
header.y + header.height / 2,
{
steps: 12,
},
);
await page.mouse.up();
await expect(page.locator(".gantt-interaction-message")).toHaveText(
"Schedule saved",
);
await expect(ledger).not.toContainText("Not scheduled");
await expect(page.locator(".task-inspector")).toHaveCount(0);
await ledger.click();
await expect(page.locator(".task-inspector")).toBeVisible();
});

const phonePrefix = "[test] bugfix3 same-prefix phone task";

for (const width of [320, 390]) {
test(`same-prefix titles remain distinguishable with aligned rows at ${width}px`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 844 });
await page.goto("/e2e/fixtures/phone-ledger.html");
await page.getByLabel(/^Search tasks/).fill(phonePrefix);
await expect(page.locator(".task-row")).toHaveCount(2);
await page.evaluate(() => document.fonts.ready);
for (const suffix of ["alpha", "beta"]) {
const title = page
.locator(".ledger-title strong")
.filter({ hasText: `${phonePrefix} ${suffix}` });
await expect(title).toHaveCSS("white-space", "normal");
expect(
await title.evaluate((element, suffix) => {
const text = element.firstChild;
const start = element.textContent!.lastIndexOf(suffix);
if (!text || start < 0) throw new Error("Title suffix is missing");
const range = document.createRange();
range.setStart(text, start);
range.setEnd(text, start + suffix.length);
const glyphs = range.getBoundingClientRect();
const bounds = element.getBoundingClientRect();
return (
glyphs.top >= bounds.top - 1 &&
glyphs.bottom <= bounds.bottom + 1 &&
glyphs.left >= bounds.left - 1 &&
glyphs.right <= bounds.right + 1
);
}, suffix),
).toBe(true);
}
for (const row of await page.locator(".task-row").all()) {
expect((await row.boundingBox())!.height).toBe(65);
expect((await row.locator(".task-ledger").boundingBox())!.height).toBe(
65,
);
expect((await row.locator(".timeline-row").boundingBox())!.height).toBe(
65,
);
const bounds = (await row.boundingBox())!;
const action = (await row
.locator(".unscheduled-row-action")
.boundingBox())!;
expect(
Math.abs(action.y + action.height / 2 - bounds.y - bounds.height / 2),
).toBeLessThanOrEqual(1);
}
await expect(page.locator(".dependency-layer")).toHaveAttribute(
"height",
"165",
);
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBeLessThanOrEqual(width);
});
}

test("row completion and reopening refresh the inspector without discarding a date draft", async ({
page,
}) => {
await page.goto("/e2e/fixtures/phone-ledger.html");
await page.getByLabel(/^Search tasks/).fill(phonePrefix);
await page.getByRole("button", { name: "Completed", exact: true }).click();
const title = `${phonePrefix} alpha`;
await page.locator(".task-ledger-main").filter({ hasText: title }).click();
const inspector = page.locator(".task-inspector");
await inspector.getByLabel("Scheduled", { exact: true }).fill("2026-11-07");
await page
.getByRole("button", { name: `Complete ${title}`, exact: true })
.click();
await expect(inspector.getByLabel(/^Status/)).toHaveValue("done");
await expect(inspector.locator(".inspector-title p")).toContainText("Done");
await expect(inspector.getByLabel("Scheduled", { exact: true })).toHaveValue(
"2026-11-07",
);
await page
.getByRole("button", { name: `Reopen ${title}`, exact: true })
.click();
await expect(inspector.getByLabel(/^Status/)).toHaveValue("open");
await expect(inspector.locator(".inspector-title p")).toContainText("Open");
await expect(inspector.getByLabel("Scheduled", { exact: true })).toHaveValue(
"2026-11-07",
);
});
Loading
Loading