~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

7
610 lines · 32.8 KB Raw History
// What people do on the pages, in order, as bob and then alice: each
// write shown at once, kept in the PDS, and shown again once the page is
// read anew, from the index.

import { expect, test } from '@playwright/test'
import { world, who, url, records, go, anew, delegate, status, switchTo, comment, editor, choose, logIn, loggedInAs } from './pages.mjs'

test.describe.configure({ mode: 'serial' })

let page

test.beforeAll(async ({ browser }) => {
  page = await browser.newPage()
})

test.afterAll(async () => {
  await page.close()
})

test('logging in through the PDS', async () => {
  await go(page, '/alice.test/tally')
  await logIn(page, 'bob')
  await expect.poll(() => loggedInAs(page)).toBe('bob.test')
})

test('starring and unstarring', async () => {
  await go(page, '/alice.test/notes')
  const star = page.getByRole('button', { name: 'Star', exact: true })
  await star.click()
  const starred = page.getByRole('button', { name: 'Starred' })
  await expect(starred).toHaveAttribute('aria-pressed', 'true')
  await expect(page.locator('.star .count')).toHaveText('1')
  expect((await records('bob', 'dev.knotless.star')).map((r) => r.value.subject?.uri ?? r.value.subject)).toContain(
    `at://${who.alice.did}/dev.knotless.repo/notes`,
  )
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Starred' })).toBeVisible())
  await page.getByRole('button', { name: 'Starred' }).click()
  await expect(page.getByRole('button', { name: 'Star', exact: true })).toHaveAttribute('aria-pressed', 'false')
  await expect(page.locator('.star .count')).toHaveText('0')
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Star', exact: true })).toBeVisible())
})

test('following and unfollowing', async () => {
  await go(page, '/textkit.test')
  await page.getByRole('button', { name: 'Follow', exact: true }).click()
  await expect(page.getByRole('button', { name: 'Unfollow' })).toHaveAttribute('aria-pressed', 'true')
  const follows = await records('bob', 'dev.knotless.follow')
  expect(follows.map((r) => r.value.subject)).toContain(who.textkit.did)
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Unfollow' })).toBeVisible())
  await page.getByRole('button', { name: 'Unfollow' }).click()
  await expect(page.getByRole('button', { name: 'Follow', exact: true })).toHaveAttribute('aria-pressed', 'false')
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Follow', exact: true })).toBeVisible())
})

let opened

test('opening an issue', async () => {
  await go(page, '/alice.test/tally/issues/new')
  await page.getByRole('textbox', { name: 'Title' }).fill('Counts a trailing newline as a line')
  await page.getByRole('textbox', { name: 'Description' }).fill('`printf "a\\n" | tally` says 1, as `wc` does. Is that meant?')
  await page.getByRole('button', { name: 'Open issue' }).click()
  await page.waitForURL(/\/alice\.test\/tally\/issue\//)
  opened = page.url()
  await expect(page.getByRole('heading', { name: 'Counts a trailing newline as a line' })).toBeVisible()
  const issues = await records('bob', 'dev.knotless.issue')
  expect(issues.map((r) => r.value.title)).toContain('Counts a trailing newline as a line')
  await go(page, '/alice.test/tally/issues')
  await anew(page, (expect) => expect(page.getByRole('link', { name: 'Counts a trailing newline as a line' })).toBeVisible())
})

test('commenting', async () => {
  await go(page, opened)
  await page.getByPlaceholder('Leave a comment').fill('Ran it against `wc -l` on a few files: they agree.')
  await page.getByRole('button', { name: 'Comment', exact: true }).click()
  await expect(page.getByText('Ran it against wc -l on a few files: they agree.')).toBeVisible()
  await expect(page.getByPlaceholder('Leave a comment')).toHaveValue('')
  const comments = await records('bob', 'dev.knotless.comment')
  expect(comments.map((r) => r.value.body)).toContain('Ran it against `wc -l` on a few files: they agree.')
  await anew(page, (expect) => expect(page.getByText('Ran it against wc -l on a few files: they agree.')).toBeVisible())
})

test('reacting', async () => {
  await go(page, opened)
  const thread = page.locator('.reactions').first()
  await thread.locator('summary[aria-label="Add reaction"]').click()
  await thread.getByRole('button', { name: 'Eyes' }).click()
  await expect(thread.getByRole('button', { name: 'Eyes: 1' })).toHaveAttribute('aria-pressed', 'true')
  expect((await records('bob', 'dev.knotless.reaction')).map((r) => r.value.content)).toContain('eyes')
  await anew(page, (expect) => expect(page.locator('.reactions').first().getByRole('button', { name: 'Eyes: 1' })).toBeVisible())
  await page.locator('.reactions').first().getByRole('button', { name: 'Eyes: 1' }).click()
  await expect(page.locator('.reactions').first().getByRole('button', { name: /^Eyes/ })).toHaveCount(0)
  expect(await records('bob', 'dev.knotless.reaction')).toEqual([])
})

test('closing and reopening', async () => {
  await go(page, opened)
  await page.getByRole('button', { name: 'Close issue' }).click()
  await expect(page.getByRole('button', { name: 'Reopen issue' })).toBeVisible()
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Reopen issue' })).toBeVisible())
  await page.getByRole('button', { name: 'Reopen issue' }).click()
  await expect(page.getByRole('button', { name: 'Close issue' })).toBeVisible()
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Close issue' })).toBeVisible())
})

test('forking', async () => {
  await go(page, '/alice.test/notes')
  await page.locator('.fork-menu summary').click()
  await expect(page.getByLabel('Name')).toHaveValue('notes')
  await page.getByRole('button', { name: 'Create fork' }).click()
  await page.waitForURL(url('/bob.test/notes'))
  await expect(page.getByText('Forked from')).toBeVisible()
  expect((await records('bob', 'dev.knotless.repo')).map((r) => r.uri.split('/').pop())).toContain('notes')
  // Every branch came with it.
  await go(page, '/bob.test/notes/branches')
  await anew(page, (expect) => expect(page.getByRole('link', { name: 'reading', exact: true })).toBeVisible())
})

test('merging a pull', async () => {
  // The fork's pull its queue ejected: merged by hand, by bob, its owner.
  await go(page, world.views['pull-ejected'])
  const box = page.locator('.merge-box')
  await box.getByRole('button', { name: 'Merge pull' }).click()
  // The page asks its answers again once merged, and the thread says so,
  // with a way to revert it, in place of the box: the box's own word for
  // it shows only until then, which can be no time at all.
  const said = page.locator('.event', { hasText: 'Merged as' })
  await expect(said.getByText(/^[0-9a-f]{7}$/)).toBeVisible()
  const sha = await said.getByText(/^[0-9a-f]{7}$/).innerText()
  await expect(page.getByRole('button', { name: 'Revert' })).toBeVisible()
  await anew(page, (expect) => expect(page.locator('.event', { hasText: 'Merged as' }).getByText(sha)).toBeVisible())
  await go(page, '/bob.test/tally/commits/main')
  await anew(page, (expect) => expect(page.getByText('Print a total for many files').first()).toBeVisible())
})

test('leaving the merge queue and joining it again', async () => {
  await go(page, world.views['pull-queued'])
  const box = page.locator('.merge-box')
  await box.getByRole('button', { name: 'Leave queue' }).click()
  await expect(box.getByRole('button', { name: 'Add to merge queue' })).toBeVisible()
  await anew(page, (expect) => expect(page.locator('.merge-box').getByRole('button', { name: 'Add to merge queue' })).toBeVisible())
  await page.locator('.merge-box').getByRole('button', { name: 'Add to merge queue' }).click()
  await expect(page.locator('.merge-box').getByRole('button', { name: 'Leave queue' })).toBeVisible()
  await anew(page, (expect) => expect(page.locator('.merge-box').getByRole('button', { name: 'Leave queue' })).toBeVisible())
})

test('reverting a merged pull', async () => {
  await go(page, world.views['pull-ejected'])
  await page.getByRole('button', { name: 'Revert' }).click()
  // On to the pull that brings the revert in.
  await page.waitForURL((u) => u.href !== url(world.views['pull-ejected']) && /\/pull\//.test(u.pathname))
  await expect(page.getByRole('heading', { name: /^Revert/ })).toBeVisible()
  await anew(page, (expect) => expect(page.getByRole('heading', { name: /^Revert/ })).toBeVisible())
})

let proposed

test('opening a pull from a fork', async () => {
  // bob's fork's main has what alice's lacks: its merges and its workflow.
  await go(page, '/alice.test/tally/pulls/new')
  await page.getByLabel('From').selectOption({ label: 'bob.test/tally (yours)' })
  await page.getByLabel('Branch').selectOption('main')
  await expect(page.locator('.compared')).toContainText('Release from tags when asked')
  // The form unfolds once asked for.
  await expect(page.getByRole('textbox', { name: 'Title' })).toHaveCount(0)
  await page.locator('.compare-ready').getByRole('button', { name: 'Open pull' }).click()
  await page.getByRole('textbox', { name: 'Title' }).fill('Bring in the fork: usage, totals and releases')
  // Asking the owner to review it as it opens, from the sidebar.
  await page.locator('.reviewer-menu summary').click()
  await page.locator('.reviewer-menu').getByRole('button', { name: 'alice.test' }).click()
  await page.locator('.reviewer-menu summary').click()
  await expect(page.locator('.side-people')).toContainText('alice.test')
  // Opened as a draft.
  await page.getByRole('button', { name: 'Open as draft' }).click()
  await page.waitForURL(/\/alice\.test\/tally\/pull\//)
  proposed = page.url()
  await expect(page.getByRole('heading', { name: 'Bring in the fork: usage, totals and releases' })).toBeVisible()
  await expect(page.locator('.item-meta .pill')).toHaveText('Draft')
  const made = await fork()
  expect(made?.value.reviewers?.map((r) => r.did)).toContain(who.alice.did)
  expect(made?.value.draft).toBe(true)
})

/** bob's pull from his fork, as his PDS holds it. */
const fork = async () => (await records('bob', 'dev.knotless.pull')).find((r) => r.value.title === 'Bring in the fork: usage, totals and releases')

test('retargeting a pull', async () => {
  await go(page, proposed)
  await page.locator('.merge-box .more-menu summary').click()
  await page.getByRole('button', { name: 'Retarget…' }).click()
  const dialog = page.getByRole('dialog', { name: 'Retarget this pull' })
  await dialog.getByLabel('Head branch').fill('words')
  await dialog.getByRole('button', { name: 'Retarget' }).click()
  await expect(dialog).toBeHidden()
  await expect(page.getByRole('alert')).toHaveCount(0)
  await anew(page, (expect) => expect(page.getByText(/wants to merge into main from bob\.test\/tally:words$/)).toBeVisible())
})

test('marking a draft ready, and making it a draft again', async () => {
  await go(page, proposed)
  const box = page.locator('.merge-box')
  await box.getByRole('button', { name: 'Ready for review' }).click()
  await expect(page.locator('.item-meta .pill')).toHaveText('Open')
  expect((await fork())?.value.draft).toBeFalsy()
  await box.locator('.more-menu summary').click()
  await box.getByRole('button', { name: 'Convert to draft' }).click()
  await expect(page.locator('.item-meta .pill')).toHaveText('Draft')
  await expect(box.getByText('This pull is still a draft')).toBeVisible()
  expect((await fork())?.value.draft).toBe(true)
  // Ready once more, for what follows.
  await box.getByRole('button', { name: 'Ready for review' }).click()
  await anew(page, (expect) => expect(page.locator('.item-meta .pill')).toHaveText('Open'))
})

test('rerunning a check', async () => {
  // bob runs his fork's checks: the failed test, asked for again.
  await go(page, world.views.check)
  await page.locator('.rerun-menu summary').click()
  await page.getByRole('button', { name: 'Re-run test' }).click()
  await expect(page.locator('.rerun-menu summary')).toHaveText(/Asked for test again/)
  expect((await records('bob', 'dev.knotless.rerun')).map((r) => r.value.workflow)).toContain('test')
})

test('running a workflow when asked', async () => {
  await go(page, '/bob.test/tally/checks')
  await page.getByRole('button', { name: 'Run workflow' }).click()
  const dialog = page.getByRole('dialog', { name: 'Run a workflow' })
  await dialog.getByLabel('Branch or tag').fill('v0.1.0')
  await dialog.getByLabel('Branch or tag').press('Tab')
  await expect(dialog.getByLabel('Workflow')).toHaveValue('release')
  await dialog.getByRole('button', { name: 'Add a variable' }).click()
  await dialog.getByRole('textbox', { name: 'Name' }).fill('CHANNEL')
  await dialog.getByRole('textbox', { name: 'Value' }).fill('beta')
  await dialog.getByRole('button', { name: 'Run workflow' }).click()
  await expect(dialog.getByRole('status')).toHaveText(/^Asked for release/)
  const asked = (await records('bob', 'dev.knotless.rerun')).find((r) => r.value.workflow === 'release')
  expect(asked?.value).toMatchObject({ ref: 'refs/tags/v0.1.0' })
})

test('editing what was written', async () => {
  await go(page, opened)
  // The issue's own text.
  await choose(page, 'Is that meant?', 'Edit')
  await editor(page, 'Update issue').fill('`printf "a\\n" | tally` says 1, as `wc -l` does. Is that meant, for a file with no newline at its end?')
  await page.getByRole('button', { name: 'Update issue' }).click()
  await expect(comment(page, 'for a file with no newline at its end?')).toBeVisible()
  // And a comment, which then says it was edited.
  await choose(page, 'they agree.', 'Edit')
  await editor(page, 'Update comment').fill('Ran it against `wc -l` on a few files: they agree, empty ones too.')
  await page.getByRole('button', { name: 'Update comment' }).click()
  await expect(comment(page, 'empty ones too.')).toContainText(', edited')
  await anew(page, async (expect) => {
    await expect(comment(page, 'for a file with no newline at its end?')).toBeVisible()
    await expect(comment(page, 'empty ones too.')).toContainText(', edited')
  })
})

test('deleting a comment', async () => {
  await go(page, opened)
  await page.getByPlaceholder('Leave a comment').fill('Said twice, by mistake.')
  await page.getByRole('button', { name: 'Comment', exact: true }).click()
  await expect(comment(page, 'Said twice, by mistake.')).toBeVisible()
  page.once('dialog', (d) => d.accept())
  await choose(page, 'Said twice, by mistake.', 'Delete')
  await expect(comment(page, 'Said twice, by mistake.')).toHaveCount(0)
  expect((await records('bob', 'dev.knotless.comment')).map((r) => r.value.body)).not.toContain('Said twice, by mistake.')
  await anew(page, (expect) => expect(comment(page, 'Said twice, by mistake.')).toHaveCount(0))
})

test('reporting a comment', async () => {
  await go(page, world.views.issue)
  await choose(page, 'Confirmed.', 'Report this comment')
  const dialog = page.getByRole('dialog', { name: /^Report/ })
  await dialog.getByLabel('More, for the moderators').fill('Testing the report flow.')
  await dialog.getByRole('button', { name: 'Send report' }).click()
  await expect(dialog.getByRole('button', { name: 'Send report' })).toHaveCount(0)
})

test('creating a repository', async () => {
  await go(page, '/new')
  await page.getByLabel('Repository name').fill('wordlist')
  await page.getByLabel(/^Description/).fill('Words to count with')
  await page.getByRole('button', { name: 'Create repository' }).click()
  await page.waitForURL(url('/bob.test/wordlist'))
  await expect(page.getByText('Words to count with')).toBeVisible()
  expect((await records('bob', 'dev.knotless.repo')).map((r) => r.uri.split('/').pop())).toContain('wordlist')
  await go(page, '/bob.test?tab=repos')
  await anew(page, (expect) => expect(page.getByRole('link', { name: 'wordlist' }).first()).toBeVisible())
})

test('updating a profile', async () => {
  await go(page, '/settings/profile')
  await page.getByLabel('Name').fill('Bob T.')
  await page.getByLabel('Bio').fill('Fixes the edges of tally.')
  await page.getByRole('button', { name: 'Update profile' }).click()
  // Saved, on to the profile it changed.
  await page.waitForURL(url('/bob.test'))
  await anew(page, async (expect) => {
    await expect(page.getByText('Bob T.').first()).toBeVisible()
    await expect(page.getByText('Fixes the edges of tally.')).toBeVisible()
  })
})

test('choosing notifications and a theme', async () => {
  await go(page, '/settings/notifications')
  // Each choice is saved as it is made, and shown as saved.
  await page.getByLabel('Only accounts I follow').click()
  await expect(page.getByLabel('Only accounts I follow')).toBeChecked()
  await anew(page, (expect) => expect(page.getByLabel('Only accounts I follow')).toBeChecked())
  await go(page, '/settings/appearance')
  // One theme always, rather than one by day and one at night as the system says.
  await page.getByLabel('One theme').click()
  await page.getByLabel('Dark', { exact: true }).click()
  await anew(page, (expect) => expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'))
  await page.getByLabel('Sync with system').click()
  await anew(page, (expect) => expect(page.locator('html')).not.toHaveAttribute('data-theme', 'dark'))
})

test('starting an organization', async () => {
  // bob's own account becomes one: he delegates writes to this server, at
  // his PDS, in a page of its own, then invites alice.
  await go(page, '/bob.test')
  await page.getByLabel('Your account').click()
  await page.getByRole('button', { name: 'Start an organization' }).click()
  await page.getByRole('dialog', { name: 'Start an organization' }).getByRole('button', { name: 'Invite people' }).click()
  await page.waitForURL(/\/bob\.test\?tab=members$/)
  await delegate(page, 'bob')
  const invite = page.locator('.org-invite')
  await invite.getByLabel('Handle').fill('alice.test')
  await invite.getByRole('button', { name: 'Invite' }).click()
  await expect(page.getByText('bob.test is now an organization. Invited alice.test as a member.')).toBeVisible()
  // Listed among the invitations, below the members, none of whom has joined.
  const invited = async (page) => {
    await expect(page.getByRole('heading', { name: 'Invitations' })).toBeVisible()
    await expect(page.getByText(/^Invited [A-Z][a-z]{2} \d+, \d{4}$/)).toBeVisible()
  }
  await invited(page)
  expect((await records('bob', 'dev.knotless.orgMember')).map((r) => r.value.member)).toContain(who.alice.did)
  await anew(page, (expect) => invited(page))
})

test('logging in to a second account and labelling as the owner', async () => {
  await go(page, opened)
  // bob, not the owner, has no labels to apply.
  await expect(page.locator('.label-menu')).toHaveCount(0)
  await logIn(page, 'alice', async () => {
    await page.getByLabel('Your account').click()
    await page.getByRole('button', { name: 'Add account' }).click()
  })
  await expect.poll(() => loggedInAs(page)).toBe('alice.test')
  await page.locator('.label-menu summary').click()
  await page.locator('.label-menu').getByRole('button', { name: 'kind/bug' }).click()
  await expect(page.locator('.label-menu').getByRole('button', { name: 'kind/bug' })).toHaveAttribute('aria-pressed', 'true')
  await anew(page, (expect) => expect(page.locator('.labels').getByText('kind/bug')).toBeVisible())
})

test('opening an issue with a label, as the owner', async () => {
  await go(page, '/alice.test/tally/issues/new')
  await page.getByRole('textbox', { name: 'Title' }).fill('Say what counts as a word in the README')
  const menu = page.locator('.open-side .label-menu')
  await menu.locator('summary').click()
  await menu.getByRole('button', { name: 'kind/bug' }).click()
  await menu.locator('summary').click()
  await expect(page.locator('.open-side .labels')).toContainText('kind/bug')
  await page.getByRole('button', { name: 'Open issue' }).click()
  await page.waitForURL(/\/alice\.test\/tally\/issue\//)
  await expect(page.getByRole('heading', { name: 'Say what counts as a word in the README' })).toBeVisible()
  await anew(page, (expect) => expect(page.locator('.item-meta .labels').getByText('kind/bug')).toBeVisible())
})

test('seeing notifications', async () => {
  await go(page, '/alice.test/tally')
  await expect(page.getByRole('link', { name: /^Notifications, \d+ new$/ })).toBeVisible()
  await go(page, '/notifications')
  await anew(page, (expect) => expect(page.getByRole('link', { name: 'Notifications', exact: true })).toBeVisible())
})

test('hiding a comment as the owner', async () => {
  await go(page, opened)
  await choose(page, 'empty ones too.', 'Hide this comment')
  await expect(comment(page, 'empty ones too.')).toHaveCount(0)
  expect((await records('alice', 'dev.knotless.hide')).length).toBeGreaterThan(0)
  await anew(page, (expect) => expect(comment(page, 'empty ones too.')).toHaveCount(0))
})

test('joining an organization', async () => {
  await go(page, '/bob.test?tab=members')
  await page.getByRole('button', { name: 'Join publicly' }).click()
  await expect(page.getByText("You're a member of bob.test, and anyone can see it.")).toBeVisible()
  // The gateway bob delegated to takes her acceptance in, as joinedAt on
  // the record of her he keeps, and her acceptance goes.
  await expect
    .poll(async () => (await records('bob', 'dev.knotless.orgMember')).find((r) => r.value.member === who.alice.did)?.value.joinedAt)
    .toBeTruthy()
  await go(page, '/alice.test')
  await anew(page, (expect) => expect(page.getByRole('link', { name: 'bob.test' }).first()).toBeVisible())
})

test('changing what a repository says about itself', async () => {
  await go(page, '/alice.test/tally/settings')
  await page.getByLabel('Description').fill('Counts lines, words and bytes, as wc does')
  await page.getByLabel('Topics').fill('rust cli text')
  await page.getByRole('button', { name: 'Save', exact: true }).click()
  await expect(page.getByRole('status')).toBeVisible()
  await go(page, '/alice.test/tally')
  await anew(page, async (expect) => {
    await expect(page.getByText('Counts lines, words and bytes, as wc does')).toBeVisible()
    await expect(page.getByRole('link', { name: 'text', exact: true })).toBeVisible()
  })
})

test('defining, changing and deleting a label', async () => {
  await go(page, '/alice.test/tally/settings/labels')
  await page.getByRole('button', { name: 'New label' }).click()
  const dialog = page.getByRole('dialog', { name: 'New label' })
  await dialog.getByLabel('Name').fill('kind/docs')
  await dialog.getByLabel(/^Description/).fill('Words, not code')
  await dialog.getByRole('button', { name: 'Create label' }).click()
  await expect(dialog).toBeHidden()
  const row = page.locator('li', { hasText: 'kind/docs' })
  await expect(row).toContainText('Words, not code')
  await row.getByRole('button', { name: 'Edit' }).click()
  const editing = page.getByRole('dialog', { name: 'Edit label' })
  await expect(editing.getByLabel('Name')).toHaveValue('kind/docs')
  await editing.getByLabel(/^Description/).fill('Words about the code')
  await editing.getByRole('button', { name: 'Save changes' }).click()
  await expect(editing).toBeHidden()
  await expect(row).toContainText('Words about the code')
  await anew(page, (expect) => expect(page.locator('li', { hasText: 'kind/docs' })).toContainText('Words about the code'))
  page.once('dialog', (d) => d.accept())
  await page.locator('li', { hasText: 'kind/docs' }).getByRole('button', { name: 'Delete' }).click()
  await expect(page.locator('li', { hasText: 'kind/docs' })).toHaveCount(0)
  await anew(page, (expect) => expect(page.locator('li', { hasText: 'kind/docs' })).toHaveCount(0))
})

test('making, protecting, renaming and deleting a branch', async () => {
  await go(page, '/alice.test/tally/settings/branches')
  await page.getByLabel('New branch').fill('topic')
  await page.getByRole('button', { name: 'Create', exact: true }).click()
  const row = (name) => page.locator('li', { has: page.getByRole('link', { name, exact: true }) })
  await expect(row('topic')).toBeVisible()
  await row('topic').getByRole('button', { name: 'Protect' }).click()
  await expect(row('topic')).toContainText('protected')
  await anew(page, (expect) => expect(row('topic')).toContainText('protected'))
  await row('topic').getByRole('button', { name: 'Unprotect' }).click()
  await expect(row('topic').getByRole('button', { name: 'Protect' })).toBeVisible()
  await row('topic').getByRole('button', { name: 'Rename' }).click()
  await page.getByLabel('New name').fill('topical')
  await row('topic').getByRole('button', { name: 'Rename' }).last().click()
  await expect(row('topical')).toBeVisible()
  await anew(page, (expect) => expect(row('topical')).toBeVisible())
  page.once('dialog', (d) => d.accept())
  await row('topical').getByRole('button', { name: 'Delete' }).click()
  await expect(row('topical')).toHaveCount(0)
  await anew(page, (expect) => expect(row('topical')).toHaveCount(0))
})

test('adding and removing a collaborator', async () => {
  await go(page, '/alice.test/tally/settings/access')
  const add = () => page.locator('form', { has: page.getByLabel('Add a collaborator') }).getByRole('button', { name: 'Add' }).click()
  await page.getByLabel('Add a collaborator').fill('bob.test')
  await add()
  // Collaborators push through a gateway: alice delegates to this one
  // first, as the page asks, then adds him again.
  await expect(page.getByRole('alert')).toContainText('delegate your writes')
  await delegate(page, 'alice')
  await add()
  const listed = page.locator('section', { hasText: 'Collaborators' }).locator('li', { hasText: 'bob.test' })
  await expect(listed).toBeVisible()
  await anew(page, (expect) => expect(page.locator('section', { hasText: 'Collaborators' }).locator('li', { hasText: 'bob.test' })).toBeVisible())
  await page.locator('section', { hasText: 'Collaborators' }).locator('li', { hasText: 'bob.test' }).getByRole('button', { name: 'Remove' }).click()
  await expect(page.locator('section', { hasText: 'Collaborators' }).locator('li', { hasText: 'bob.test' })).toHaveCount(0)
  await anew(page, (expect) => expect(page.locator('section', { hasText: 'Collaborators' }).locator('li', { hasText: 'bob.test' })).toHaveCount(0))
})

test('naming and removing a triager', async () => {
  await go(page, '/alice.test/tally/settings/access')
  await page.getByLabel('Add a triager').fill('bob.test')
  await page.getByLabel('Scopes').fill('kind')
  await page.locator('form', { has: page.getByLabel('Scopes') }).getByRole('button', { name: 'Add' }).click()
  const named = (page) => page.locator('section', { hasText: 'Triagers' }).locator('li', { hasText: 'bob.test' })
  await expect(named(page)).toBeVisible()
  await anew(page, (expect) => expect(named(page)).toBeVisible())
  await named(page).getByRole('button', { name: 'Remove' }).click()
  await expect(named(page)).toHaveCount(0)
  await anew(page, (expect) => expect(named(page)).toHaveCount(0))
})

test('adding and removing a secret', async () => {
  await go(page, '/alice.test/tally/settings/actions')
  await page.getByLabel('New secret').fill('DEPLOY_TOKEN')
  await page.getByLabel('Value').fill('not-a-real-token')
  await page.getByRole('button', { name: 'Add secret' }).click()
  const listed = (page) => page.locator('li', { hasText: 'DEPLOY_TOKEN' })
  await expect(listed(page)).toBeVisible()
  await anew(page, (expect) => expect(listed(page)).toBeVisible())
  page.once('dialog', (d) => d.accept())
  await listed(page).getByRole('button').click()
  await expect(listed(page)).toHaveCount(0)
  await anew(page, (expect) => expect(listed(page)).toHaveCount(0))
})

test('compacting a repository', async () => {
  await go(page, '/alice.test/tally/settings')
  await page.getByRole('button', { name: 'Compact' }).click()
  await expect(page.getByRole('button', { name: 'Compacting…' })).toHaveCount(0, { timeout: 30_000 })
  await expect(page.getByRole('button', { name: 'Compact' })).toBeVisible()
  await expect(page.locator('section', { hasText: 'Storage' }).getByRole('alert')).toHaveCount(0)
})

test('merging into a protected branch', async () => {
  // alice's main is protected and linear: the pull is squashed into one commit.
  await go(page, world.views['pull-open'])
  await expect(page.locator('.merge-box')).toContainText('it is squashed into one commit')
  await page.locator('.merge-box').getByRole('button', { name: 'Merge pull' }).click()
  const said = page.locator('.event', { hasText: 'Merged as' })
  await expect(said.getByText(/^[0-9a-f]{7}$/)).toBeVisible()
  await go(page, '/alice.test/tally/commits/main')
  await anew(page, (expect) => expect(page.getByText('Print counts as JSON with --json').first()).toBeVisible())
})

test('renaming a repository', async () => {
  await go(page, '/alice.test/scratch/settings')
  await page.getByLabel('Name', { exact: true }).fill('sketches')
  await page.getByRole('button', { name: 'Rename' }).click()
  await page.waitForURL(/\/alice\.test\/sketches/)
  // The old name leads to the new.
  await anew(page, async (expect) => {
    await go(page, '/alice.test/scratch')
    expect(new URL(page.url()).pathname).toMatch(/^\/alice\.test\/sketches/)
  })
})

test('offering a repository to another account', async () => {
  await go(page, '/alice.test/sketches/settings')
  await page.getByRole('combobox', { name: 'Account' }).fill('bob.test')
  await page.getByRole('button', { name: 'Offer it' }).click()
  await expect(page.getByRole('button', { name: 'Withdraw offer' })).toBeVisible()
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Withdraw offer' })).toBeVisible())
})

test('leaving an organization', async () => {
  await go(page, '/bob.test?tab=members')
  await page.locator('li, .member', { hasText: 'alice.test' }).locator('details summary').first().click()
  await page.getByRole('button', { name: 'Leave bob.test' }).click()
  await page.getByRole('dialog').getByRole('button', { name: 'Leave', exact: true }).click()
  await expect(page.getByText("You've left bob.test.")).toBeVisible()
  await expect
    .poll(async () => (await records('bob', 'dev.knotless.orgMember')).some((r) => r.value.member === who.alice.did))
    .toBe(false)
})

test('taking up a transfer', async () => {
  await switchTo(page, 'bob.test')
  await go(page, '/alice.test/sketches')
  await page.getByRole('button', { name: 'Accept transfer' }).click()
  await page.waitForURL(/\/bob\.test\/sketches/)
  await anew(page, (expect) => expect(page.getByRole('heading').filter({ hasText: 'bob.test' }).first()).toBeVisible())
})

test('ending an organization', async () => {
  await go(page, '/settings/organization')
  await page.getByRole('button', { name: 'Delete', exact: true }).click()
  const dialog = page.getByRole('dialog', { name: 'Delete organization' })
  await dialog.getByRole('textbox').fill('bob.test')
  await dialog.getByRole('button', { name: 'Delete organization' }).click()
  await expect.poll(async () => (await records('bob', 'dev.knotless.org')).length).toBe(0)
  await go(page, '/bob.test')
  await anew(page, (expect) => expect(page.getByRole('link', { name: /^Members/ })).toHaveCount(0))
})

test('archiving and deleting a repository', async () => {
  await go(page, '/bob.test/wordlist/settings')
  await page.getByRole('button', { name: 'Archive', exact: true }).click()
  await expect(page.getByRole('button', { name: 'Unarchive' })).toBeVisible()
  await anew(page, (expect) => expect(page.getByRole('button', { name: 'Unarchive' })).toBeVisible())
  await page.getByRole('button', { name: 'Delete', exact: true }).click()
  const dialog = page.locator('#delete-repo')
  await dialog.getByRole('textbox').fill('bob.test/wordlist')
  await dialog.getByRole('button', { name: 'Delete this repository' }).click()
  await page.waitForURL((u) => !u.pathname.startsWith('/bob.test/wordlist'))
  expect((await records('bob', 'dev.knotless.repo')).map((r) => r.uri.split('/').pop())).not.toContain('wordlist')
  await expect.poll(async () => status(page, '/bob.test/wordlist')).toBe(404)
})

test("deleting one's knotless data", async () => {
  await go(page, '/settings/profile')
  await page.getByRole('button', { name: 'Delete data' }).click()
  const dialog = page.getByRole('dialog', { name: 'Delete your knotless data' })
  await dialog.getByRole('textbox').fill('bob.test')
  await dialog.getByRole('button', { name: 'Delete all my knotless data' }).click()
  // The page deletes in parts, then goes home.
  await page.waitForURL((u) => u.pathname === '/', { timeout: 30_000 })
  await expect.poll(async () => (await records('bob', 'dev.knotless.repo')).length, { timeout: 30_000 }).toBe(0)
  await expect.poll(async () => status(page, '/bob.test/tally')).toBe(404)
  // It logs bob out too: the browser goes on as alice.
  await anew(page, async (expect) => expect(await loggedInAs(page)).toBe('alice.test'))
})

test('logging out', async () => {
  await go(page, '/alice.test/notes')
  await expect.poll(() => loggedInAs(page)).toBe('alice.test')
  await page.getByLabel('Your account').click()
  await page.getByRole('button', { name: 'Log out' }).click()
  await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible()
  // Starring asks who is starring first.
  await go(page, '/alice.test/notes')
  await page.getByRole('button', { name: 'Star', exact: true }).click()
  await expect(page.getByRole('dialog', { name: 'Log in with your Atmosphere account' })).toBeVisible()
})