// 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('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() await expect.poll(async () => (await records('bob', 'dev.knotless.issue')).map((r) => r.value.body).join('\n')).toContain('- [ ] 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(1)).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. 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() })