Projects live at URLs, have owners, and are edited together live
A project is only ever at /p/<id>/<slug>; / is the index of the projects
you own or edit. Every project has an owner, who can name editors;
anyone with the link can view. Every edit saves itself, one request in
flight at a time, as a patch of the leaves that changed, and a websocket
(channels + daphne) carries presence and each committed write to
everyone else in the project. The first write to a leaf wins, and the
loser is told.
Undo is per person: a step undoes only if the leaves it touched still
hold what it left, so it never takes a collaborator's work with it.
Named snapshots replace saving, and restore as an ordinary write.
An empty symbol now survives the leaf round trip with `:nodes {}`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c17ee138f2
commit
6a53adb5e0
31 changed files with 1918 additions and 158 deletions
|
|
@ -303,10 +303,27 @@ async function main() {
|
|||
if (!probe) throw new Error('no canvas.stage on the page — ' +
|
||||
(page.logs.slice(0, 3).join(' | ') || 'is `shadow-cljs watch app` running?'));
|
||||
|
||||
console.log(`\ncanvas ${probe.w}x${probe.h}, ${probe.doc}`);
|
||||
// `/` is the index, and a project is only ever at its own address, so the
|
||||
// suite signs up, makes one, and opens it — the page's own navigation, so
|
||||
// this CDP session and its console stay attached.
|
||||
const home = await page.eval(`(async () => {
|
||||
const post = (url, body) => fetch(url, {method: 'POST', body: JSON.stringify(body),
|
||||
headers: {'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.cookie.match(/csrftoken=([^;]+)/)[1]}}).then(r => r.json());
|
||||
await post('/api/signup', {username: 'suite-' + Date.now().toString(36), password: 'password1'});
|
||||
const made = await post('/api/projects', {name: 'untitled'});
|
||||
arthur.events.collab.navigate_BANG_(arthur.events.collab.project_path(made.id, made.name));
|
||||
return location.pathname; })()`);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
probe = await page.eval(PROBE);
|
||||
if (/opened/.test(probe.doc)) break;
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
check(probe.drawn === 0 && /new document/.test(probe.doc),
|
||||
'the app opens on a blank document',
|
||||
console.log(`\ncanvas ${probe.w}x${probe.h}, ${probe.doc} at ${home}`);
|
||||
|
||||
check(probe.drawn === 0 && /opened untitled/.test(probe.doc),
|
||||
'a new project opens on a blank document',
|
||||
`${probe.drawn} px drawn — ${probe.doc}`);
|
||||
check(probe.w === 320 && probe.h === 200, 'the canvas is the stage size',
|
||||
`${probe.w}x${probe.h}`);
|
||||
|
|
@ -432,9 +449,10 @@ async function main() {
|
|||
await sleep(150);
|
||||
const sent = await sample(FRAMES);
|
||||
|
||||
check(await page.eval(CLICK('save')), 'save is clickable');
|
||||
// There is no save: every edit saves itself, and a built-in example opened
|
||||
// in a project becomes a project of its own at once.
|
||||
const saved = await statusMatching(/saved r\d+/);
|
||||
check(saved !== null, 'the document saves', saved ?? (await page.eval(STATUS)));
|
||||
check(saved !== null, 'the take becomes a saved project by itself', saved ?? (await page.eval(STATUS)));
|
||||
// Upload count may be zero when the content-addressed blocks already exist
|
||||
// on this server. The saved clip must still reference them.
|
||||
const savedBlocks = await page.eval(`(async () => {
|
||||
|
|
@ -445,14 +463,16 @@ async function main() {
|
|||
check(savedBlocks > 0, 'the saved clip references its tier 2 blocks',
|
||||
`${savedBlocks} blocks`);
|
||||
|
||||
// Again, unchanged. Content addressing means the second save uploads nothing
|
||||
// and rewrites nothing: this is the assertion that the keys are stable across
|
||||
// two independent freezes of the same take, and that an unchanged leaf keeps
|
||||
// its version rather than being rewritten.
|
||||
check(await page.eval(CLICK('save')), 'save is clickable again');
|
||||
const resaved = await statusMatching(/saved r\d+ · 0 leaves · 0 blocks/);
|
||||
check(resaved !== null, 'saving an unchanged document writes nothing',
|
||||
resaved ?? (await page.eval(STATUS)));
|
||||
// Unchanged, nothing is written: the project's seq stands still. This is
|
||||
// the assertion that the keys are stable and a clean document is clean.
|
||||
const seqOf = `(async () => {
|
||||
const id = location.pathname.split('/')[2];
|
||||
return (await fetch('/api/projects/' + id).then(r => r.json())).seq; })()`;
|
||||
const seqBefore = await page.eval(seqOf);
|
||||
await sleep(800);
|
||||
const seqAfter = await page.eval(seqOf);
|
||||
check(seqBefore === seqAfter, 'an unchanged document writes nothing',
|
||||
`seq ${seqBefore} -> ${seqAfter}`);
|
||||
|
||||
check(await fromMenu(MENU_PICK_NEWEST), 'open is clickable');
|
||||
const opened = await statusMatching(/opened /);
|
||||
|
|
@ -550,9 +570,8 @@ async function main() {
|
|||
.find(el => el.textContent.startsWith('key 8 → 16'));
|
||||
return label?.querySelector('select')?.value === 'linear';
|
||||
})()`), 'the second drawing gap can be set to tween');
|
||||
check(await page.eval(CLICK('save')), 'the painted document can be saved');
|
||||
check((await statusMatching(/saved r\d+ · \d+ leaves/)) !== null,
|
||||
'the painted shape is saved', await page.eval(STATUS));
|
||||
check((await statusMatching(/saved r\d+/)) !== null,
|
||||
'the painted shape saves by itself', await page.eval(STATUS));
|
||||
check(await fromMenu(MENU_PICK_NEWEST), 'the painted document can be reopened');
|
||||
check((await statusMatching(/opened /)) !== null,
|
||||
'the painted shape is reopened', await page.eval(STATUS));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue