164 lines
6.8 KiB
TypeScript
164 lines
6.8 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|
import { importUser } from './testUtils';
|
|
|
|
/**
|
|
* A project reached through a shared link arrives with its handle shortened:
|
|
* the link carries "01ABC..." where a project opened from a list carries
|
|
* "https://endorser.ch/entity/01ABC...". Whatever the page holds at that moment
|
|
* is what it signs into every claim made from it, and the server matches the
|
|
* full handle, so a short ID leaves a claim recorded and attached to nothing:
|
|
* the gift is accepted, reports a success, and never appears on the project.
|
|
*
|
|
* Spec 37 records the same gifts on a project opened from the discover list,
|
|
* where the handle is already full. Only the way in differs here, and that is
|
|
* the whole point: these tests would have passed while the shared-link route
|
|
* was broken, so they are worth their own file rather than a parameter on 37.
|
|
*/
|
|
|
|
/** The prefix the server puts in front of an entity's ID to make its handle. */
|
|
const ENTITY_HANDLE_PREFIX = 'https://endorser.ch/entity/';
|
|
|
|
/**
|
|
* Open a project from the discover list and return its ID in both forms: the
|
|
* full handle the app holds when it came from a list, and the short form a
|
|
* shared link carries.
|
|
*/
|
|
async function findProjectIds(
|
|
page: Page,
|
|
): Promise<{ fullId: string; shortId: string }> {
|
|
await page.goto('./discover');
|
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
|
await page.locator('ul#listDiscoverResults li:first-child a').click();
|
|
await page.waitForURL(/\/project\//);
|
|
|
|
const fullId = decodeURIComponent(
|
|
new URL(page.url()).pathname.substring('/project/'.length),
|
|
);
|
|
expect(fullId).toContain(ENTITY_HANDLE_PREFIX);
|
|
|
|
return { fullId, shortId: fullId.substring(ENTITY_HANDLE_PREFIX.length) };
|
|
}
|
|
|
|
/**
|
|
* Open a project the way a shared link does, so the page holds only the short
|
|
* ID. The redirect page sends a desktop browser straight on to the project,
|
|
* which is the same place the native handler routes to.
|
|
*/
|
|
async function openProjectByDeepLink(page: Page, shortId: string) {
|
|
await page.goto('./deep-link/project/' + shortId);
|
|
// The whole pathname, because "/deep-link/project/<id>" also ends in
|
|
// "/project/<id>" and a looser wait returns before the redirect happens.
|
|
await page.waitForURL((url) => url.pathname === '/project/' + shortId);
|
|
}
|
|
|
|
/**
|
|
* Record a gift on the open project and check that the project shows it.
|
|
*
|
|
* @param isToProject true gives to the project, false gives from it. The two
|
|
* travel in different fields of the claim -- what the gift fulfills, and who
|
|
* provided it -- so a short ID can break one and leave the other working.
|
|
*/
|
|
async function recordGiftOnOpenProject(page: Page, isToProject: boolean) {
|
|
const selector = isToProject ? 'gives-to' : 'gives-from';
|
|
const finalTitle = 'Gift ' + Math.random().toString(36).substring(2, 6);
|
|
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
|
|
|
await page.getByTestId(selector).locator('div:first-child div button').click();
|
|
await page.getByPlaceholder('What was given').fill(finalTitle);
|
|
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
|
|
|
// Anything short of a plain success means the server could not record part
|
|
// of the claim, which is exactly how an unlinked gift used to be reported.
|
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
|
|
|
// The list is built from a report query that matches the project's full
|
|
// handle, so this is the assertion that the claim was linked and not merely
|
|
// stored.
|
|
// ("li" rather than "div ul li": the two sections nest their lists
|
|
// differently, and gives-from puts its ul directly under the test id.)
|
|
await page.reload();
|
|
await expect(
|
|
page.getByTestId(selector).locator('li').filter({ hasText: finalTitle }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
test('Record a give from a project opened by its shared-link ID', async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(60000);
|
|
await importUser(page, '00');
|
|
const { shortId } = await findProjectIds(page);
|
|
await openProjectByDeepLink(page, shortId);
|
|
await recordGiftOnOpenProject(page, false);
|
|
});
|
|
|
|
test('Record a give to a project opened by its shared-link ID', async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(60000);
|
|
await importUser(page, '00');
|
|
const { shortId } = await findProjectIds(page);
|
|
await openProjectByDeepLink(page, shortId);
|
|
await recordGiftOnOpenProject(page, true);
|
|
});
|
|
|
|
test('Record an offer on a project opened by its shared-link ID', async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(60000);
|
|
const description = `Offering of ${Math.random().toString(36).substring(2, 5)}`;
|
|
|
|
await importUser(page, '00');
|
|
const { shortId } = await findProjectIds(page);
|
|
await openProjectByDeepLink(page, shortId);
|
|
|
|
// An offer carries the project in its own field, and the buttons that open
|
|
// these dialogs render before the project finishes loading, so an offer made
|
|
// straight after landing is the case most likely to escape with a short ID.
|
|
await expect(page.getByTestId('offerButton')).toBeVisible();
|
|
await page.getByTestId('offerButton').click();
|
|
await page.getByTestId('inputDescription').fill(description);
|
|
await page
|
|
.getByTestId('inputOfferAmount')
|
|
.locator('input')
|
|
.fill(String(Math.floor(Math.random() * 998) + 1));
|
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
|
await page.getByRole('alert').filter({ hasText: 'Success' }).getByRole('button').click();
|
|
|
|
// The project's own offer list comes from a report query on the plan, so the
|
|
// description showing up here says the project reference survived. Reading
|
|
// the offer's claim instead would prove nothing: the claim holds whatever was
|
|
// signed, linked or not.
|
|
await page.reload();
|
|
await expect(
|
|
page.getByTestId('offers-to').locator('li').filter({ hasText: description }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('A shared project link lands on the project holding only the short ID', async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(60000);
|
|
await importUser(page, '00');
|
|
const { fullId, shortId } = await findProjectIds(page);
|
|
|
|
await page.goto('./deep-link/project/' + shortId);
|
|
await page.waitForURL((url) => url.pathname.startsWith('/project/'));
|
|
|
|
// This is where the short ID comes from, and the condition the three tests
|
|
// above depend on. If shared links ever carry the full handle instead, this
|
|
// is the test that should fail first, because those three would still pass
|
|
// while no longer describing anything real.
|
|
const landedId = decodeURIComponent(
|
|
new URL(page.url()).pathname.substring('/project/'.length),
|
|
);
|
|
expect(landedId).toEqual(shortId);
|
|
expect(landedId).not.toEqual(fullId);
|
|
|
|
// The project still has to load from that short ID, or the page is useless
|
|
// whatever it does with claims.
|
|
await expect(page.getByTestId('offerButton')).toBeVisible();
|
|
});
|