Files
crowd-funder-for-time-pwa/test-playwright/38-record-gift-on-deep-linked-project.spec.ts

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();
});