johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
// 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 { writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, test } from '@playwright/test'
import {
world,
who,
url,
records,
go,
anew,
knotless,
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('watching a repository, and unsubscribing from an issue', async () => {
await go(page, '/alice.test/notes')
const watch = page.locator('.watch-menu > summary')
await expect(watch).toHaveText('Watch')
await watch.click()
await page.getByRole('button', { name: /^All activity/ }).click()
await expect(watch).toHaveText('Watching')
await anew(page, (expect) => expect(page.locator('.watch-menu > summary')).toHaveText('Watching'))
await page.locator('.watch-menu > summary').click()
await page.getByRole('button', { name: /^Participating/ }).click()
await expect(page.locator('.watch-menu > summary')).toHaveText('Watch')
await anew(page, (expect) => expect(page.locator('.watch-menu > summary')).toHaveText('Watch'))
// Its author hears of the issue, until it unsubscribes, which mutes it.
await go(page, opened)
await page.getByRole('button', { name: 'Unsubscribe' }).click()
await expect(page.getByRole('button', { name: 'Subscribe', exact: true })).toHaveAttribute(
'aria-pressed',
'false',
)
await anew(page, (expect) =>
expect(page.getByRole('button', { name: 'Subscribe', exact: true })).toHaveAttribute(
'title',
'You muted this issue.',
),
)
await page.getByRole('button', { name: 'Subscribe', exact: true }).click()
await expect(page.getByRole('button', { name: 'Unsubscribe' })).toHaveAttribute(
'aria-pressed',
'true',
)
await anew(page, (expect) =>
expect(page.getByRole('button', { name: 'Unsubscribe' })).toHaveAttribute(
'title',
'You hear of this issue as its author.',
),
)
})
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,
// with the queue turned off for main. The rule is back once it merged.
const fork = 'knotless://bob.test/tally'
knotless('bob', 'branch', 'protect', fork, 'main', '--undo')
await go(page, world.views['pull-ejected'])
await anew(page, (expect) =>
expect(page.locator('.merge-box').getByRole('button', { name: 'Merge pull' })).toBeVisible(),
)
await page.locator('.merge-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. A merge is
// several writes to the PDS, as compacting is, so it gets as long.
const said = page.locator('.event', { hasText: 'Merged as' })
await expect(said.getByText(/^[0-9a-f]{7}$/)).toBeVisible({ timeout: 30_000 })
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(),
)
knotless('bob', 'branch', 'protect', fork, 'main', '--merges', '--queue')
})
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('adding a stack to the merge queue', async () => {
const fork = 'knotless://bob.test/tally'
const layer = (branch, on) => {
knotless('bob', 'branch', 'create', fork, branch, on)
knotless(
'bob',
'repo',
'commit',
fork,
'-b',
branch,
'-m',
`Add ${branch}`,
'--put',
`${branch}.txt`,
'/dev/null',
)
return knotless('bob', 'pull', 'create', fork, on, fork, branch, `Layer ${branch}`)
}
const low = layer('stack-low', 'main')
const high = layer('stack-high', 'stack-low')
const page_of = (uri) => `/bob.test/tally/pull/${who.bob.did}/${uri.split('/').pop()}`
const box = () => page.locator('.merge-box')
const queued = async () =>
(await records('bob', 'dev.knotless.queue'))
.filter((r) => r.value.state === 'queued')
.map((r) => r.value.subject?.uri ?? r.value.subject)
await go(page, page_of(high))
await anew(page, (expect) =>
expect(box().getByRole('button', { name: 'Add all 2 to merge queue' })).toBeEnabled(),
)
await box().getByRole('button', { name: 'Add all 2 to merge queue' }).click()
await expect(box().getByRole('button', { name: 'Leave queue' })).toBeVisible()
expect(await queued()).toEqual(expect.arrayContaining([low, high]))
await anew(page, (expect) =>
expect(
page.locator('.merge-box').getByText('Waits for the pull below it to land'),
).toBeVisible(),
)
expect(knotless('bob', 'queue', 'list', fork)).toContain(`stacked\t${high}`)
// The lowest leaves, and the one waiting on it goes too.
await go(page, page_of(low))
await anew(page, (expect) =>
expect(box().getByRole('button', { name: 'Leave queue' })).toBeVisible(),
)
await box().getByRole('button', { name: 'Leave queue' }).click()
await expect(box().getByRole('button', { name: 'Add to merge queue' })).toBeVisible()
await expect.poll(() => knotless('bob', 'queue', 'list', fork)).not.toContain(high)
knotless('bob', 'pull', 'close', fork, high)
knotless('bob', 'pull', 'close', fork, low)
})
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 Add beside the buttons.
await page.locator('.open-form .add-menu summary').click()
await page.locator('.open-form .add-menu').getByRole('button', { name: 'alice.test' }).click()
await expect(page.locator('.open-form .reviewers-fact')).toContainText('1 reviewer')
// 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)
// Asked no longer, then again, once it is open, from under its title.
await page.locator('.reviewers-fact summary').click()
await page.getByRole('button', { name: 'Stop asking alice.test' }).click()
await expect(page.locator('.reviewers-fact')).toHaveCount(0)
await expect.poll(async () => (await fork())?.value.reviewers ?? []).toHaveLength(0)
await page.locator('.add-menu summary').click()
await page.locator('.add-menu').getByRole('button', { name: 'alice.test' }).click()
await expect(page.locator('.reviewers-fact')).toContainText('1 reviewer')
await expect
.poll(async () => (await fork())?.value.reviewers?.map((r) => r.did))
.toContain(who.alice.did)
})
/** 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('ticking a task in what was written', async () => {
await go(page, opened)
await choose(page, 'for a file with no newline at its end?', '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?\n\n- [ ] Try an empty file\n- [ ] Try a file with no newline',
)
await page.getByRole('button', { name: 'Update issue' }).click()
const box = comment(page, 'for a file with no newline at its end?').getByRole('checkbox').nth(1)
await expect(box).toBeEnabled()
await box.check()
await expect(box).toBeChecked()
// Its text ticks it too, at once, while the first is still saving.
await comment(page, 'for a file with no newline at its end?')
.getByText('Try an empty file')
.click()
await expect(
comment(page, 'for a file with no newline at its end?').getByRole('checkbox').nth(0),
).toBeChecked()
await expect
.poll(async () =>
(await records('bob', 'dev.knotless.issue')).map((r) => r.value.body).join('\n'),
)
.toContain('- [x] Try an empty file\n- [x] Try a file with no newline')
await anew(page, (expect) =>
expect(
comment(page, 'for a file with no newline at its end?').getByRole('checkbox').nth(0),
).toBeChecked(),
)
})
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, as an admin.
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.getByLabel('Role').selectOption('admin')
await invite.getByRole('button', { name: 'Invite' }).click()
await expect(
page.getByText('bob.test is now an organization. Invited alice.test as an admin.'),
).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')
// With none yet, a label is put on from Add, then shows under the title.
await page.locator('.add-menu summary').click()
await page.locator('.add-menu').getByRole('button', { name: 'kind/bug' }).click()
await expect(page.locator('.item-facts .labels')).toContainText('kind/bug')
await page.locator('.item-facts .label-menu summary').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())
// Then assigned from Add to its author, kept in alice's assignment record.
await page.locator('.add-menu summary').click()
await page.locator('.add-menu').getByRole('button', { name: 'bob.test' }).click()
await expect(page.locator('.assignees-fact')).toContainText('Assigned to bob.test')
const assigned = async () =>
(await records('alice', 'dev.knotless.assignment'))
.flatMap((r) => r.value.assignees ?? [])
.map((a) => a.did)
await expect.poll(assigned).toContain(who.bob.did)
await anew(page, (expect) =>
expect(page.locator('.assignees-fact')).toContainText('Assigned to bob.test'),
)
})
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')
await page.locator('.open-form .add-menu summary').click()
await page.locator('.open-form .add-menu').getByRole('button', { name: 'kind/bug' }).click()
await expect(page.locator('.open-form .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(),
)
// A pull or an issue muted from its row, and that undone.
const rows = page.locator('.notifications li')
const at = await rows.evaluateAll((ls) => ls.findIndex((l) => l.querySelector('.row-mute')))
expect(at).toBeGreaterThanOrEqual(0)
const row = rows.nth(at)
await row.hover()
await row.locator('.row-mute').click()
await expect(row.getByRole('status')).toContainText(/^You muted this (issue|pull)\./)
// Read back as the page asks, through the gateway only the browser reaches.
const chosen = () =>
page.evaluate(async () =>
(await (await fetch('/xrpc/dev.knotless.listSubscriptions')).json()).subscriptions.map(
(s) => s.level,
),
)
expect(await chosen()).toContain('muted')
await row.getByRole('button', { name: 'Undo' }).click()
await expect(row.locator('.row-mute')).toHaveCount(1)
expect(await chosen()).not.toContain('muted')
})
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 an organization's repository as its admin", async () => {
// alice, an admin of bob.test now, changes its repository, written as
// bob.test by the gateway he delegated to, but neither transfers nor
// deletes it.
await go(page, '/bob.test/wordlist')
await page.getByRole('link', { name: 'Settings' }).click()
await page.waitForURL(/\/bob\.test\/wordlist\/settings$/)
await page.getByLabel('Description').fill('Words, one a line')
await page.getByRole('button', { name: 'Save', exact: true }).click()
await expect(page.getByRole('status')).toBeVisible()
const wordlist = async () =>
(await records('bob', 'dev.knotless.repo')).find((r) => r.uri.endsWith('/wordlist'))?.value
expect((await wordlist()).description).toBe('Words, one a line')
await expect(
page.getByText('Only bob.test itself can transfer or delete this repository.'),
).toBeVisible()
await expect(page.getByRole('button', { name: 'Delete', exact: true })).toHaveCount(0)
await anew(page, (expect) =>
expect(page.getByLabel('Description')).toHaveValue('Words, one a line'),
)
})
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('naming and removing a gate of a branch', async () => {
await go(page, '/alice.test/tally/settings/branches')
await page.getByLabel('New branch').fill('gated')
await page.getByRole('button', { name: 'Create', exact: true }).click()
const row = (page) =>
page.locator('li', { has: page.getByRole('link', { name: 'gated', exact: true }) })
await row(page).getByRole('button', { name: 'Protect' }).click()
await expect(row(page)).toContainText('protected')
await row(page).getByLabel('Required reviewers').fill('bob.test')
await row(page)
.locator('form', { has: page.getByLabel('Required reviewers') })
.getByRole('button', { name: 'Add' })
.click()
const gate = (page) => row(page).locator('.gates li', { hasText: 'bob.test' })
await expect(gate(page)).toBeVisible()
await anew(page, (expect) => expect(gate(page)).toBeVisible())
// The rule in alice's record names bob by DID.
const rule = async () => {
const repo = (await records('alice', 'dev.knotless.repo')).find((r) => r.uri.endsWith('/tally'))
return repo.value.rules.find((r) => r.branch === 'gated')
}
expect((await rule()).reviews.gates).toEqual([{ did: who.bob.did }])
await gate(page).getByRole('button', { name: 'Remove' }).click()
await expect(gate(page)).toHaveCount(0)
await anew(page, (expect) => expect(gate(page)).toHaveCount(0))
expect((await rule()).reviews).toBeUndefined()
await row(page).getByRole('button', { name: 'Unprotect' }).click()
await expect(row(page).getByRole('button', { name: 'Protect' })).toBeVisible()
page.once('dialog', (d) => d.accept())
await row(page).getByRole('button', { name: 'Delete' }).click()
await expect(row(page)).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 to one first with Delegate writes to this server',
)
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('asking for changes, then dismissing the review, as the owner', async () => {
await go(page, world.views['pull-open'])
await page.getByPlaceholder('Leave a comment').fill('Name the field `chars` too.')
await page.getByLabel('Request changes').check()
await page.getByRole('button', { name: 'Request changes', exact: true }).click()
const box = page.locator('.merge-box')
await expect(box).toContainText('Changes requested')
// From the merge box's row for the review, saying why.
await box.locator('.reviewers .action-menu summary').first().click()
await box.getByRole('button', { name: 'Dismiss review' }).click()
const dialog = page.getByRole('dialog', { name: 'Dismiss review' })
await dialog.getByLabel('Why').fill('Settled in chat.')
await dialog.getByRole('button', { name: 'Dismiss review' }).click()
await expect(box).toContainText('review dismissed')
await expect(page.locator('.comment', { hasText: 'Settled in chat.' })).toContainText(
"dismissed alice.test's review",
)
const dismissal = async () =>
(await records('alice', 'dev.knotless.comment')).find(
(r) => r.value.body === 'Settled in chat.',
)?.value.dismisses
await expect.poll(dismissal).toMatch(/^at:\/\/.*\/dev\.knotless\.comment\//)
await anew(page, (expect) =>
expect(page.locator('.comment', { hasText: 'Name the field' }).locator('.verdict')).toHaveText(
'Dismissed',
),
)
})
test('setting a pull to merge when its checks pass, and cancelling it', async () => {
// bob's pull from his fork has no checks, so merges now: he adds one
// no runner runs here, and alice, who owns the repository, sets it to
// merge once it passes.
const workflow = join(tmpdir(), 'pull-test.yml')
writeFileSync(
workflow,
'on: {pull: {base: [refs/heads/main]}}\nsteps:\n - name: test\n run: cargo test\n',
)
knotless(
'bob',
'repo',
'commit',
'knotless://bob.test/tally',
'-b',
'words',
'-m',
'Test each pull',
'--put',
'.knotless/workflows/test.yml',
workflow,
)
await go(page, proposed)
const box = page.locator('.merge-box')
await anew(page, (expect) =>
expect(box.getByRole('button', { name: 'Merge when checks pass' })).toBeVisible(),
)
await box.getByRole('button', { name: 'Merge when checks pass' }).click()
await expect(box).toContainText('Merges when checks pass')
await expect(box).toContainText('Set by alice.test.')
const set = async () =>
(await records('alice', 'dev.knotless.autoMerge')).map((r) => r.value.state)
await expect.poll(set).toEqual(['set'])
await anew(page, (expect) =>
expect(page.locator('.merge-box')).toContainText('Merges when checks pass'),
)
// Cancelled, it merges only when someone merges it.
await page.locator('.merge-box').getByRole('button', { name: 'Cancel merge' }).click()
await expect(
page.locator('.merge-box').getByRole('button', { name: 'Merge when checks pass' }),
).toBeVisible()
await expect.poll(async () => (await set()).sort()).toEqual(['cancelled', 'set'])
await anew(page, (expect) =>
expect(page.locator('.merge-box')).not.toContainText('Merges when checks pass'),
)
})
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()
})