// A private repository used on the pages, over the spaces alpha: alice's // `vault`, made private from the command line by setup.mjs, and one she // makes from the pages. Its records live in its space, not in anyone's // public repo, and its pages read them from this server's index of that // space, apart from the appview's. So each write here is checked on the // page at once, from the space with the command line, as not in the // writer's public repo, on the page read anew, and as not there at all to // a browser logged in to no one. import { expect, test } from '@playwright/test' import { who, url, records, go, anew, knotless, seen, switchTo, comment, editor, choose, logIn, loggedInAs } from './pages.mjs' test.describe.configure({ mode: 'serial' }) const vault = 'knotless://alice.test/vault' /// The record a pull's or issue's page, at `href`, shows. const uriOf = (href) => { const [kind, did, rkey] = new URL(href).pathname.split('/').slice(-3) return `at://${did}/dev.knotless.${kind}/${rkey}` } /// The page as the members see it, and one for a browser logged in to no /// one, which finds nothing of it. let page let stranger test.beforeAll(async ({ browser }) => { page = await browser.newPage() stranger = await browser.newPage() }) test.afterAll(async () => { await page.close() await stranger.close() }) test('logging in as its owner', async () => { // To anyone, it is not there. await go(page, '/alice.test/vault') await expect(page.getByText('Not Found')).toBeVisible() await logIn(page, 'alice') await expect.poll(() => loggedInAs(page)).toBe('alice.test') await anew(page, async (expect) => { await expect(page.getByText('Plans not yet for anyone else.')).toBeVisible() await expect(page.locator('.tag.private').first()).toBeVisible() }) }) test('creating a private repository', async () => { await go(page, '/new') await page.getByLabel('Repository name').fill('diary') await page.getByRole('radio', { name: /^Private/ }).click() await page.getByRole('button', { name: 'Create repository' }).click() await page.waitForURL(url('/alice.test/diary')) await expect(page.locator('.tag.private').first()).toBeVisible() expect(knotless('alice', 'ls', 'alice.test')).toMatch(/^diary\t.*private$/m) expect((await records('alice', 'dev.knotless.repo')).map((r) => r.uri.split('/').pop())).not.toContain('diary') expect(await seen(stranger, '/alice.test/diary')).toBe(404) await go(page, '/alice.test?tab=repos') await anew(page, (expect) => expect(page.getByRole('link', { name: 'diary' }).first()).toBeVisible()) }) test('defining a label', async () => { await go(page, '/alice.test/vault/settings/labels') await page.getByRole('button', { name: 'New label' }).click() const dialog = page.getByRole('dialog', { name: 'New label' }) await dialog.getByLabel('Name').fill('kind/secret') await dialog.getByRole('button', { name: 'Create label' }).click() await expect(dialog).toBeHidden() await expect(page.locator('li', { hasText: 'kind/secret' })).toBeVisible() expect(knotless('alice', 'labels', vault)).toContain('kind/secret') await anew(page, (expect) => expect(page.locator('li', { hasText: 'kind/secret' })).toBeVisible()) }) let pulled test('opening a pull from a branch', async () => { await go(page, '/alice.test/vault/pulls/new') await page.getByLabel('From').selectOption({ label: 'alice.test/vault (yours)' }) await page.getByLabel('Branch').selectOption('plans') await expect(page.locator('.compared')).toContainText('Write down the plans') await expect(page.getByRole('textbox', { name: 'Title' })).toBeHidden() await page.locator('.compare-ready').getByRole('button', { name: 'Open pull' }).click() await page.getByRole('textbox', { name: 'Title' }).fill('Write down the plans') await page.getByRole('button', { name: 'Open pull' }).click() await page.waitForURL(/\/alice\.test\/vault\/pull\//) pulled = page.url() await expect(page.getByRole('heading', { name: 'Write down the plans' })).toBeVisible() expect(knotless('alice', 'list', vault, '--pulls')).toContain('Write down the plans') expect((await records('alice', 'dev.knotless.pull')).map((r) => r.value.title)).not.toContain('Write down the plans') expect(await seen(stranger, new URL(pulled).pathname)).toBe(404) await go(page, '/alice.test/vault/pulls') await anew(page, (expect) => expect(page.getByRole('link', { name: 'Write down the plans' })).toBeVisible()) }) const readers = (page) => page.locator('section', { hasText: 'Readers' }).locator('li', { hasText: 'bob.test' }) test('adding a reader', async () => { await go(page, '/alice.test/vault/settings/access') await page.getByLabel('Add a reader').fill('bob.test') await page.locator('form', { has: page.getByLabel('Add a reader') }).getByRole('button', { name: 'Add' }).click() await expect(readers(page)).toBeVisible() expect(knotless('alice', 'members', vault)).toContain(`${who.bob.did}\tread`) await anew(page, (expect) => expect(readers(page)).toBeVisible()) }) test('logging in as a reader', async () => { await go(page, '/alice.test/vault') await logIn(page, 'bob', async () => { await page.getByLabel('Your account').click() await page.getByRole('button', { name: 'Add account' }).click() }) await expect.poll(() => loggedInAs(page)).toBe('bob.test') await anew(page, (expect) => expect(page.getByText('Plans not yet for anyone else.')).toBeVisible()) // Not the owner: no settings. await expect(page.getByRole('link', { name: 'Settings', exact: true })).toHaveCount(0) }) let asked test('opening an issue as a reader', async () => { await go(page, '/alice.test/vault/issues/new') await page.getByRole('textbox', { name: 'Title' }).fill('The plans need dates') await page.getByRole('textbox', { name: 'Description' }).fill('Without them, nothing ships.') await page.getByRole('button', { name: 'Open issue' }).click() await page.waitForURL(/\/alice\.test\/vault\/issue\//) asked = page.url() await expect(page.getByRole('heading', { name: 'The plans need dates' })).toBeVisible() // In the space, where alice reads it, and not in bob's public repo. expect(knotless('alice', 'list', vault, '--issues')).toContain('The plans need dates') expect((await records('bob', 'dev.knotless.issue')).map((r) => r.value.title)).not.toContain('The plans need dates') expect(await seen(stranger, new URL(asked).pathname)).toBe(404) await go(page, '/alice.test/vault/issues') await anew(page, (expect) => expect(page.getByRole('link', { name: 'The plans need dates' })).toBeVisible()) }) test('commenting, reacting and editing as a reader', async () => { await go(page, asked) await page.getByPlaceholder('Leave a comment').fill('By the end of the month?') await page.getByRole('button', { name: 'Comment', exact: true }).click() await expect(comment(page, 'By the end of the month?')).toBeVisible() expect(knotless('alice', 'show', vault, uriOf(asked))).toContain('By the end of the month?') expect((await records('bob', 'dev.knotless.comment')).map((r) => r.value.body)).not.toContain('By the end of the month?') await anew(page, (expect) => expect(comment(page, 'By the end of the month?')).toBeVisible()) 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')).toEqual([]) await anew(page, (expect) => expect(page.locator('.reactions').first().getByRole('button', { name: 'Eyes: 1' })).toBeVisible()) await choose(page, 'By the end of the month?', 'Edit') await editor(page, 'Update comment').fill('By the end of next month?') await page.getByRole('button', { name: 'Update comment' }).click() await expect(comment(page, 'By the end of next month?')).toContainText(', edited') await anew(page, (expect) => expect(comment(page, 'By the end of next month?')).toContainText(', edited')) }) test('commenting on a pull as a reader', async () => { await go(page, pulled) await page.getByPlaceholder('Leave a comment').fill('Looks right to me.') await page.getByRole('button', { name: 'Comment', exact: true }).click() await expect(comment(page, 'Looks right to me.')).toBeVisible() await anew(page, (expect) => expect(comment(page, 'Looks right to me.')).toBeVisible()) }) test('hearing of it as the owner', async () => { await switchTo(page, 'alice.test') await go(page, '/notifications') await anew(page, (expect) => expect(page.getByText('The plans need dates').first()).toBeVisible()) }) test('labelling, closing and hiding as the owner', async () => { await go(page, asked) await page.locator('.label-menu summary').click() await page.locator('.label-menu').getByRole('button', { name: 'kind/secret' }).click() await expect(page.locator('.label-menu').getByRole('button', { name: 'kind/secret' })).toHaveAttribute('aria-pressed', 'true') await anew(page, (expect) => expect(page.locator('.labels').getByText('kind/secret')).toBeVisible()) 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()) expect(knotless('alice', 'list', vault, '--issues')).toMatch(/^issue\tclosed\t.*\tThe plans need dates$/m) 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()) await choose(page, 'By the end of next month?', 'Hide this comment') await expect(comment(page, 'By the end of next month?')).toHaveCount(0) await anew(page, (expect) => expect(comment(page, 'By the end of next month?')).toHaveCount(0)) }) test('merging a private pull', async () => { await go(page, pulled) 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 anew(page, (expect) => expect(page.locator('.event', { hasText: 'Merged as' })).toBeVisible()) expect(knotless('alice', 'refs', vault)).toMatch(/refs\/heads\/main/) await go(page, '/alice.test/vault/commits/main') await anew(page, (expect) => expect(page.getByText('Write down the plans').first()).toBeVisible()) }) test('forking a private repository', async () => { await switchTo(page, 'bob.test') await go(page, '/alice.test/vault') await page.locator('.fork-menu summary').click() await page.getByRole('button', { name: 'Create fork' }).click() await page.waitForURL(url('/bob.test/vault')) await expect(page.getByText('Forked from')).toBeVisible() await expect(page.locator('.tag.private').first()).toBeVisible() // Private as what it forked: in no public repo, and to no one else. expect((await records('bob', 'dev.knotless.repo')).map((r) => r.uri.split('/').pop())).not.toContain('vault') expect(await seen(stranger, '/bob.test/vault')).toBe(404) await anew(page, (expect) => expect(page.getByText('Plans not yet for anyone else.')).toBeVisible()) }) test('removing a reader', async () => { await switchTo(page, 'alice.test') await go(page, '/alice.test/vault/settings/access') await readers(page).getByRole('button', { name: 'Remove' }).click() await expect(readers(page)).toHaveCount(0) expect(knotless('alice', 'members', vault)).not.toContain(who.bob.did) await anew(page, (expect) => expect(readers(page)).toHaveCount(0)) // bob reads it no more, once the server asks the space again who may, // which it does now and then rather than on every look. await switchTo(page, 'bob.test') await go(page, '/alice.test/vault') await anew(page, (expect) => expect(page.getByText('Not Found')).toBeVisible(), 60_000) }) test('renaming and deleting a private repository', async () => { await switchTo(page, 'alice.test') await go(page, '/alice.test/diary/settings') await page.getByLabel('Name', { exact: true }).fill('journal') await page.getByRole('button', { name: 'Rename' }).click() await page.waitForURL(/\/alice\.test\/journal/) expect(knotless('alice', 'ls', 'alice.test')).toMatch(/^journal\t.*private$/m) await anew(page, async (expect) => { await go(page, '/alice.test/diary') expect(new URL(page.url()).pathname).toMatch(/^\/alice\.test\/journal/) }) await go(page, '/alice.test/journal/settings') await page.getByRole('button', { name: 'Delete', exact: true }).click() const dialog = page.locator('#delete-repo') await dialog.getByRole('textbox').fill('alice.test/journal') await dialog.getByRole('button', { name: 'Delete this repository' }).click() await page.waitForURL((u) => !u.pathname.startsWith('/alice.test/journal')) expect(knotless('alice', 'ls', 'alice.test')).not.toMatch(/^journal\t/m) await go(page, '/alice.test/journal') await anew(page, (expect) => expect(page.getByText('Not Found')).toBeVisible()) })