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

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The e2e flows fork, merge and queue, and wait for each page to be ready

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:harness/e2e-more
.knotless/workflows/e2e.yml+2−2
@@ -1,7 +1,7 @@
# The pages used as people use them, as `just e2e` runs them: in Chromium,
# logging in through the PDS's own approval page, then starring,
# following, opening an issue, commenting, reacting, closing and
# labelling, each read back on the page and from the PDS
# following, opening an issue, commenting, reacting, closing, forking,
# merging, queueing and labelling, each read back on the page and from the PDS
# (harness/e2e/flows.spec.mjs), over a world seeded for the run as the
# shots' is. The shots show what the pages draw; this, that what they do
# lands. Each flow that fails leaves the page as it was then among the
docs/development.md+1−0
@@ -213,6 +213,7 @@ Chromium, as bob and then alice, they:
- log in through the PDS's own approval page
- star, follow, open an issue, comment, react, close and reopen
- fork, merge a pull, and leave the merge queue and join it again
- label as the repository's owner, logged in to a second account
- switch back, and log out of each
harness/e2e/flows.spec.mjs+65−11
@@ -16,11 +16,22 @@ const records = async (name, collection) => {
return (await fetch(asked).then((r) => r.json())).records ?? []
}
/// Until the page's script has taken it over, as the root says once
/// mounted: a click before that reaches no handler.
const ready = (page) => page.locator('html[data-ready]').waitFor({ state: 'attached' })
/// `page` at `path` of the pages, ready.
const go = async (page, path) => {
await page.goto(path.startsWith('http') ? path : url(path))
await ready(page)
}
/// Until `check` passes on `page` read anew: the index follows the PDS a
/// moment behind.
const anew = (page, check) =>
expect(async () => {
await page.reload()
await ready(page)
await check()
}).toPass({ timeout: 15_000 })
@@ -38,6 +49,7 @@ const logIn = async (page, name, open = () => page.getByRole('button', { name: '
await page.getByRole('button', { name: 'Sign in' }).click()
await page.getByRole('button', { name: /^(accept|authorize|allow)$/i }).click()
await page.waitForURL(back)
await ready(page)
await expect(page.getByLabel('Your account')).toBeVisible()
}
@@ -60,13 +72,13 @@ test.afterAll(async () => {
})
test('logging in through the PDS', async () => {
await page.goto(url('/alice.test/tally'))
await go(page, '/alice.test/tally')
await logIn(page, 'bob')
await expect.poll(() => loggedInAs(page)).toBe('bob.test')
})
test('starring and unstarring', async () => {
await page.goto(url('/alice.test/notes'))
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' })
@@ -83,7 +95,7 @@ test('starring and unstarring', async () => {
})
test('following and unfollowing', async () => {
await page.goto(url('/textkit.test'))
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')
@@ -97,7 +109,7 @@ test('following and unfollowing', async () => {
let opened
test('opening an issue', async () => {
await page.goto(url('/alice.test/tally/issues/new'))
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()
@@ -106,12 +118,12 @@ test('opening an issue', async () => {
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 page.goto(url('/alice.test/tally/issues'))
await go(page, '/alice.test/tally/issues')
await anew(page, () => expect(page.getByRole('link', { name: 'Counts a trailing newline as a line' })).toBeVisible())
})
test('commenting', async () => {
await page.goto(opened)
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()
@@ -122,7 +134,7 @@ test('commenting', async () => {
})
test('reacting', async () => {
await page.goto(opened)
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()
@@ -135,7 +147,7 @@ test('reacting', async () => {
})
test('closing and reopening', async () => {
await page.goto(opened)
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(page.getByRole('button', { name: 'Reopen issue' })).toBeVisible())
@@ -144,8 +156,49 @@ test('closing and reopening', async () => {
await anew(page, () => 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(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(page.locator('.event', { hasText: 'Merged as' }).getByText(sha)).toBeVisible())
await go(page, '/bob.test/tally/commits/main')
await anew(page, () => 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(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(page.locator('.merge-box').getByRole('button', { name: 'Leave queue' })).toBeVisible())
})
test('logging in to a second account and labelling as the owner', async () => {
await page.goto(opened)
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 () => {
@@ -160,7 +213,7 @@ test('logging in to a second account and labelling as the owner', async () => {
})
test('switching back and logging out', async () => {
await page.goto(opened)
await go(page, opened)
await page.getByLabel('Your account').click()
await page.locator('.account .menu-item.switch', { hasText: 'bob.test' }).click()
await expect.poll(() => loggedInAs(page)).toBe('bob.test')
@@ -173,7 +226,8 @@ test('switching back and logging out', async () => {
await page.getByRole('button', { name: 'Log out' }).click()
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible()
// Starring asks who is starring first.
await page.goto(url('/alice.test/notes'))
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()
})
AGENTS.md+1−0
@@ -218,6 +218,7 @@ on every pull. `main` deploys only once they all pass:
the run as the shots' is, served as a gateway behind the harness's
https front: logging in through the PDS's approval page, then
starring, following, opening an issue, commenting, reacting, closing,
forking, merging a pull, leaving and joining the merge queue,
labelling as the owner, switching accounts and logging out, each read
back on the page and from the PDS (`harness/e2e/flows.spec.mjs`). A
flow that fails leaves its page among the check's outputs. A new
Justfile+3−2
@@ -195,8 +195,9 @@ shots-up: web
tail -f /dev/null | node harness/shots/world.mjs
# The pages used as people use them, in a browser: logging in through the
# PDS, then starring, following, opening, commenting, reacting, closing and
# labelling, each read back on the page and from the PDS
# PDS, then starring, following, opening, commenting, reacting, closing,
# forking, merging, queueing and labelling, each read back on the page and
# from the PDS
# (harness/e2e/flows.spec.mjs), over a world of its own seeded as the
# shots' is. Arguments go to Playwright: -g 'logging in through|starring'
# for one flow, after the login it needs.